@reportforge/playwright-pdf 0.22.1 → 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/index.mjs CHANGED
@@ -1229,13 +1229,13 @@ var require_ast = __commonJS({
1229
1229
  helperExpression: function helperExpression(node) {
1230
1230
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1231
1231
  },
1232
- scopedId: function scopedId(path17) {
1233
- return /^\.|this\b/.test(path17.original);
1232
+ scopedId: function scopedId(path16) {
1233
+ return /^\.|this\b/.test(path16.original);
1234
1234
  },
1235
1235
  // an ID is simple if it only has one part, and that part is not
1236
1236
  // `..` or `this`.
1237
- simpleId: function simpleId(path17) {
1238
- return path17.parts.length === 1 && !AST.helpers.scopedId(path17) && !path17.depth;
1237
+ simpleId: function simpleId(path16) {
1238
+ return path16.parts.length === 1 && !AST.helpers.scopedId(path16) && !path16.depth;
1239
1239
  }
1240
1240
  }
1241
1241
  };
@@ -2309,12 +2309,12 @@ var require_helpers2 = __commonJS({
2309
2309
  loc
2310
2310
  };
2311
2311
  }
2312
- function prepareMustache(path17, params, hash, open, strip, locInfo) {
2312
+ function prepareMustache(path16, params, hash, open, strip, locInfo) {
2313
2313
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
2314
2314
  var decorator = /\*/.test(open);
2315
2315
  return {
2316
2316
  type: decorator ? "Decorator" : "MustacheStatement",
2317
- path: path17,
2317
+ path: path16,
2318
2318
  params,
2319
2319
  hash,
2320
2320
  escaped,
@@ -2634,9 +2634,9 @@ var require_compiler = __commonJS({
2634
2634
  },
2635
2635
  DecoratorBlock: function DecoratorBlock(decorator) {
2636
2636
  var program = decorator.program && this.compileProgram(decorator.program);
2637
- var params = this.setupFullMustacheParams(decorator, program, void 0), path17 = decorator.path;
2637
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
2638
2638
  this.useDecorators = true;
2639
- this.opcode("registerDecorator", params.length, path17.original);
2639
+ this.opcode("registerDecorator", params.length, path16.original);
2640
2640
  },
2641
2641
  PartialStatement: function PartialStatement(partial) {
2642
2642
  this.usePartial = true;
@@ -2700,46 +2700,46 @@ var require_compiler = __commonJS({
2700
2700
  }
2701
2701
  },
2702
2702
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
2703
- var path17 = sexpr.path, name = path17.parts[0], isBlock = program != null || inverse != null;
2704
- this.opcode("getContext", path17.depth);
2703
+ var path16 = sexpr.path, name = path16.parts[0], isBlock = program != null || inverse != null;
2704
+ this.opcode("getContext", path16.depth);
2705
2705
  this.opcode("pushProgram", program);
2706
2706
  this.opcode("pushProgram", inverse);
2707
- path17.strict = true;
2708
- this.accept(path17);
2707
+ path16.strict = true;
2708
+ this.accept(path16);
2709
2709
  this.opcode("invokeAmbiguous", name, isBlock);
2710
2710
  },
2711
2711
  simpleSexpr: function simpleSexpr(sexpr) {
2712
- var path17 = sexpr.path;
2713
- path17.strict = true;
2714
- this.accept(path17);
2712
+ var path16 = sexpr.path;
2713
+ path16.strict = true;
2714
+ this.accept(path16);
2715
2715
  this.opcode("resolvePossibleLambda");
2716
2716
  },
2717
2717
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
2718
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path17 = sexpr.path, name = path17.parts[0];
2718
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path16 = sexpr.path, name = path16.parts[0];
2719
2719
  if (this.options.knownHelpers[name]) {
2720
2720
  this.opcode("invokeKnownHelper", params.length, name);
2721
2721
  } else if (this.options.knownHelpersOnly) {
2722
2722
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
2723
2723
  } else {
2724
- path17.strict = true;
2725
- path17.falsy = true;
2726
- this.accept(path17);
2727
- this.opcode("invokeHelper", params.length, path17.original, _ast2["default"].helpers.simpleId(path17));
2724
+ path16.strict = true;
2725
+ path16.falsy = true;
2726
+ this.accept(path16);
2727
+ this.opcode("invokeHelper", params.length, path16.original, _ast2["default"].helpers.simpleId(path16));
2728
2728
  }
2729
2729
  },
2730
- PathExpression: function PathExpression(path17) {
2731
- this.addDepth(path17.depth);
2732
- this.opcode("getContext", path17.depth);
2733
- var name = path17.parts[0], scoped = _ast2["default"].helpers.scopedId(path17), blockParamId = !path17.depth && !scoped && this.blockParamIndex(name);
2730
+ PathExpression: function PathExpression(path16) {
2731
+ this.addDepth(path16.depth);
2732
+ this.opcode("getContext", path16.depth);
2733
+ var name = path16.parts[0], scoped = _ast2["default"].helpers.scopedId(path16), blockParamId = !path16.depth && !scoped && this.blockParamIndex(name);
2734
2734
  if (blockParamId) {
2735
- this.opcode("lookupBlockParam", blockParamId, path17.parts);
2735
+ this.opcode("lookupBlockParam", blockParamId, path16.parts);
2736
2736
  } else if (!name) {
2737
2737
  this.opcode("pushContext");
2738
- } else if (path17.data) {
2738
+ } else if (path16.data) {
2739
2739
  this.options.data = true;
2740
- this.opcode("lookupData", path17.depth, path17.parts, path17.strict);
2740
+ this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
2741
2741
  } else {
2742
- this.opcode("lookupOnContext", path17.parts, path17.falsy, path17.strict, scoped);
2742
+ this.opcode("lookupOnContext", path16.parts, path16.falsy, path16.strict, scoped);
2743
2743
  }
2744
2744
  },
2745
2745
  StringLiteral: function StringLiteral(string) {
@@ -3095,16 +3095,16 @@ var require_util = __commonJS({
3095
3095
  }
3096
3096
  exports.urlGenerate = urlGenerate;
3097
3097
  function normalize(aPath) {
3098
- var path17 = aPath;
3098
+ var path16 = aPath;
3099
3099
  var url = urlParse(aPath);
3100
3100
  if (url) {
3101
3101
  if (!url.path) {
3102
3102
  return aPath;
3103
3103
  }
3104
- path17 = url.path;
3104
+ path16 = url.path;
3105
3105
  }
3106
- var isAbsolute = exports.isAbsolute(path17);
3107
- var parts = path17.split(/\/+/);
3106
+ var isAbsolute = exports.isAbsolute(path16);
3107
+ var parts = path16.split(/\/+/);
3108
3108
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
3109
3109
  part = parts[i];
3110
3110
  if (part === ".") {
@@ -3121,15 +3121,15 @@ var require_util = __commonJS({
3121
3121
  }
3122
3122
  }
3123
3123
  }
3124
- path17 = parts.join("/");
3125
- if (path17 === "") {
3126
- path17 = isAbsolute ? "/" : ".";
3124
+ path16 = parts.join("/");
3125
+ if (path16 === "") {
3126
+ path16 = isAbsolute ? "/" : ".";
3127
3127
  }
3128
3128
  if (url) {
3129
- url.path = path17;
3129
+ url.path = path16;
3130
3130
  return urlGenerate(url);
3131
3131
  }
3132
- return path17;
3132
+ return path16;
3133
3133
  }
3134
3134
  exports.normalize = normalize;
3135
3135
  function join(aRoot, aPath) {
@@ -5932,8 +5932,8 @@ var require_printer = __commonJS({
5932
5932
  return this.accept(sexpr.path) + " " + params + hash;
5933
5933
  };
5934
5934
  PrintVisitor.prototype.PathExpression = function(id) {
5935
- var path17 = id.parts.join("/");
5936
- return (id.data ? "@" : "") + "PATH:" + path17;
5935
+ var path16 = id.parts.join("/");
5936
+ return (id.data ? "@" : "") + "PATH:" + path16;
5937
5937
  };
5938
5938
  PrintVisitor.prototype.StringLiteral = function(string) {
5939
5939
  return '"' + string.value + '"';
@@ -6053,7 +6053,7 @@ var require_win32 = __commonJS({
6053
6053
  "node_modules/chrome-finder/lib/win32.js"(exports, module) {
6054
6054
  "use strict";
6055
6055
  init_esm_shims();
6056
- var path17 = __require("path");
6056
+ var path16 = __require("path");
6057
6057
  var { canAccess } = require_util2();
6058
6058
  function win32() {
6059
6059
  const installations = [];
@@ -6067,7 +6067,7 @@ var require_win32 = __commonJS({
6067
6067
  const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
6068
6068
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
6069
6069
  if (prefix) {
6070
- const chromePath = path17.join(prefix, suffix);
6070
+ const chromePath = path16.join(prefix, suffix);
6071
6071
  if (canAccess(chromePath)) {
6072
6072
  installations.push(chromePath);
6073
6073
  }
@@ -6085,7 +6085,7 @@ var require_darwin = __commonJS({
6085
6085
  "use strict";
6086
6086
  init_esm_shims();
6087
6087
  var { execSync: execSync3 } = __require("child_process");
6088
- var path17 = __require("path");
6088
+ var path16 = __require("path");
6089
6089
  var { canAccess, newLineRegex, sort } = require_util2();
6090
6090
  function darwin() {
6091
6091
  const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
@@ -6095,7 +6095,7 @@ var require_darwin = __commonJS({
6095
6095
  ${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
6096
6096
  `).toString().split(newLineRegex).forEach((inst) => {
6097
6097
  suffixes.forEach((suffix) => {
6098
- const execPath = path17.join(inst.trim(), suffix);
6098
+ const execPath = path16.join(inst.trim(), suffix);
6099
6099
  if (canAccess(execPath)) {
6100
6100
  installations.push(execPath);
6101
6101
  }
@@ -6123,8 +6123,8 @@ var require_linux = __commonJS({
6123
6123
  "node_modules/chrome-finder/lib/linux.js"(exports, module) {
6124
6124
  "use strict";
6125
6125
  init_esm_shims();
6126
- var { execSync: execSync3, execFileSync: execFileSync2 } = __require("child_process");
6127
- var path17 = __require("path");
6126
+ var { execSync: execSync3, execFileSync } = __require("child_process");
6127
+ var path16 = __require("path");
6128
6128
  var fs16 = __require("fs");
6129
6129
  var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
6130
6130
  function findChromeExecutablesForLinuxDesktop(folder) {
@@ -6142,7 +6142,7 @@ var require_linux = __commonJS({
6142
6142
  function linux() {
6143
6143
  let installations = [];
6144
6144
  const desktopInstallationFolders = [
6145
- path17.join(__require("os").homedir(), ".local/share/applications/"),
6145
+ path16.join(__require("os").homedir(), ".local/share/applications/"),
6146
6146
  "/usr/share/applications/"
6147
6147
  ];
6148
6148
  desktopInstallationFolders.forEach((folder) => {
@@ -6180,7 +6180,7 @@ var require_linux = __commonJS({
6180
6180
  return;
6181
6181
  }
6182
6182
  try {
6183
- const chromePath = execFileSync2("which", [executable]).toString().split(newLineRegex)[0];
6183
+ const chromePath = execFileSync("which", [executable]).toString().split(newLineRegex)[0];
6184
6184
  if (canAccess(chromePath)) {
6185
6185
  installations.push(chromePath);
6186
6186
  }
@@ -6750,8 +6750,8 @@ function getErrorMap() {
6750
6750
  // node_modules/zod/v3/helpers/parseUtil.js
6751
6751
  init_esm_shims();
6752
6752
  var makeIssue = (params) => {
6753
- const { data, path: path17, errorMaps, issueData } = params;
6754
- const fullPath = [...path17, ...issueData.path || []];
6753
+ const { data, path: path16, errorMaps, issueData } = params;
6754
+ const fullPath = [...path16, ...issueData.path || []];
6755
6755
  const fullIssue = {
6756
6756
  ...issueData,
6757
6757
  path: fullPath
@@ -6871,11 +6871,11 @@ var errorUtil;
6871
6871
 
6872
6872
  // node_modules/zod/v3/types.js
6873
6873
  var ParseInputLazyPath = class {
6874
- constructor(parent, value, path17, key) {
6874
+ constructor(parent, value, path16, key) {
6875
6875
  this._cachedPath = [];
6876
6876
  this.parent = parent;
6877
6877
  this.data = value;
6878
- this._path = path17;
6878
+ this._path = path16;
6879
6879
  this._key = key;
6880
6880
  }
6881
6881
  get path() {
@@ -10520,6 +10520,21 @@ var liveConfigSchema = external_exports.object({
10520
10520
  // Debounce window for batched flushes (ms).
10521
10521
  flushMs: external_exports.number().int().min(500).max(1e4).default(2e3)
10522
10522
  }).default({});
10523
+ var redactConfigSchema = external_exports.object({
10524
+ enabled: external_exports.boolean().default(false),
10525
+ builtins: external_exports.boolean().default(true),
10526
+ patterns: external_exports.array(
10527
+ external_exports.string().refine((v) => {
10528
+ try {
10529
+ new RegExp(v);
10530
+ return true;
10531
+ } catch {
10532
+ return false;
10533
+ }
10534
+ }, "Each redact.patterns entry must be a valid regular expression")
10535
+ ).default([]),
10536
+ mask: external_exports.string().default("[REDACTED]")
10537
+ }).default({});
10523
10538
  var sectionFlagsShape = Object.fromEntries(
10524
10539
  SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
10525
10540
  );
@@ -10540,7 +10555,7 @@ var ReporterOptionsSchema = external_exports.object({
10540
10555
  primaryColor: hexColor.optional().default(DEFAULT_OPTIONS.primaryColor),
10541
10556
  accentColor: hexColor.optional().default(DEFAULT_OPTIONS.accentColor),
10542
10557
  watermark: external_exports.string().optional(),
10543
- pdfPassword: external_exports.string().optional(),
10558
+ pdfPassword: external_exports.string().min(1, "pdfPassword must not be empty. Omit the option (or leave the env var unset) to disable encryption.").optional(),
10544
10559
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
10545
10560
  projectName: external_exports.string().optional(),
10546
10561
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
@@ -10573,6 +10588,7 @@ var ReporterOptionsSchema = external_exports.object({
10573
10588
  }, "requirementTagPattern must be a valid regular expression").optional().default(DEFAULT_OPTIONS.requirementTagPattern),
10574
10589
  failureAnalysis: failureAnalysisConfigSchema,
10575
10590
  capture: captureConfigSchema,
10591
+ redact: redactConfigSchema,
10576
10592
  live: liveConfigSchema,
10577
10593
  templatePath: external_exports.union([
10578
10594
  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"),
@@ -12615,7 +12631,7 @@ var ShardMerger = class {
12615
12631
  // src/pdf/PdfGenerator.ts
12616
12632
  init_esm_shims();
12617
12633
  import fs14 from "fs";
12618
- import path14 from "path";
12634
+ import path13 from "path";
12619
12635
 
12620
12636
  // src/templates/engine.ts
12621
12637
  init_esm_shims();
@@ -13150,49 +13166,38 @@ var ChartRenderer = class {
13150
13166
 
13151
13167
  // src/pdf/PdfEncryptor.ts
13152
13168
  init_esm_shims();
13153
- import { execFileSync } from "child_process";
13154
13169
  import fs11 from "fs";
13155
- import path11 from "path";
13156
- import os10 from "os";
13157
- import crypto3 from "crypto";
13170
+ import { PDFDocument, PDFHeader } from "@cantoo/pdf-lib";
13171
+ var AES256_HEADER_MINOR = "7ext3";
13158
13172
  var PdfEncryptor = class {
13159
13173
  async encrypt(pdfPath, password) {
13160
13174
  if (!password) return;
13161
- if (!this.isQpdfAvailable()) {
13162
- logger.warn(
13163
- "pdfPassword is set but qpdf is not installed \u2014 PDF will NOT be encrypted. Install qpdf: https://qpdf.readthedocs.io/en/stable/installation.html"
13164
- );
13165
- return;
13166
- }
13167
- const tmpPath = path11.join(os10.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
13168
- const argFile = path11.join(os10.tmpdir(), `rf-qpdf-args-${crypto3.randomBytes(8).toString("hex")}`);
13169
13175
  try {
13170
- const args = ["--encrypt", password, password, "256", "--", pdfPath, tmpPath];
13171
- fs11.writeFileSync(argFile, args.join("\n"), { mode: 384 });
13172
- execFileSync("qpdf", [`@${argFile}`], { stdio: "pipe" });
13173
- fs11.renameSync(tmpPath, pdfPath);
13174
- logger.info(`PDF encrypted with password.`);
13176
+ const doc = await PDFDocument.load(await fs11.promises.readFile(pdfPath));
13177
+ doc.context.header = PDFHeader.forVersion(1, AES256_HEADER_MINOR);
13178
+ doc.encrypt({
13179
+ userPassword: password,
13180
+ ownerPassword: password,
13181
+ permissions: { printing: "highResolution", copying: false, modifying: false }
13182
+ });
13183
+ const encryptedBytes = await doc.save();
13184
+ const tmpPath = `${pdfPath}.rf-enc-tmp`;
13185
+ try {
13186
+ await fs11.promises.writeFile(tmpPath, encryptedBytes);
13187
+ await fs11.promises.rename(tmpPath, pdfPath);
13188
+ } finally {
13189
+ fs11.rmSync(tmpPath, { force: true });
13190
+ }
13191
+ logger.info("PDF encrypted with password (AES-256).");
13175
13192
  } catch (e) {
13176
- throw new Error(`qpdf encryption failed: ${e.message}`);
13177
- } finally {
13178
- if (fs11.existsSync(tmpPath)) fs11.unlinkSync(tmpPath);
13179
- fs11.rmSync(argFile, { force: true });
13180
- }
13181
- }
13182
- isQpdfAvailable() {
13183
- try {
13184
- execFileSync("qpdf", ["--version"], { stdio: "pipe" });
13185
- return true;
13186
- } catch (err) {
13187
- logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
13188
- return false;
13193
+ throw new Error(`PDF encryption failed: ${e.message}`);
13189
13194
  }
13190
13195
  }
13191
13196
  };
13192
13197
 
13193
13198
  // src/filename/FilenameResolver.ts
13194
13199
  init_esm_shims();
13195
- import path12 from "path";
13200
+ import path11 from "path";
13196
13201
  import fs12 from "fs";
13197
13202
  var MAX_BRANCH_LENGTH = 50;
13198
13203
  var FilenameResolver = class {
@@ -13210,8 +13215,8 @@ var FilenameResolver = class {
13210
13215
  const branch = this.sanitiseBranch(ctx.branch);
13211
13216
  const status = ctx.status;
13212
13217
  const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
13213
- const absolute = path12.isAbsolute(resolved) ? resolved : path12.resolve(process.cwd(), resolved);
13214
- fs12.mkdirSync(path12.dirname(absolute), { recursive: true });
13218
+ const absolute = path11.isAbsolute(resolved) ? resolved : path11.resolve(process.cwd(), resolved);
13219
+ fs12.mkdirSync(path11.dirname(absolute), { recursive: true });
13215
13220
  return absolute;
13216
13221
  }
13217
13222
  /**
@@ -13223,7 +13228,7 @@ var FilenameResolver = class {
13223
13228
  * // → '/out/2026-report-executive.pdf'
13224
13229
  */
13225
13230
  injectTemplateSuffix(outputPath, suffix) {
13226
- const ext = path12.extname(outputPath);
13231
+ const ext = path11.extname(outputPath);
13227
13232
  const base = outputPath.slice(0, outputPath.length - ext.length);
13228
13233
  return `${base}-${suffix}${ext}`;
13229
13234
  }
@@ -13282,7 +13287,12 @@ var PRESETS = {
13282
13287
  // src/pdf/FailurePaginator.ts
13283
13288
  init_esm_shims();
13284
13289
  import fs13 from "fs";
13285
- import path13 from "path";
13290
+ import path12 from "path";
13291
+ function sidecarPathFor(pdfPath) {
13292
+ const dir = path12.dirname(pdfPath);
13293
+ const base = path12.basename(pdfPath, path12.extname(pdfPath));
13294
+ return path12.join(dir, `${base}-failures.json`);
13295
+ }
13286
13296
  var FailurePaginator = class {
13287
13297
  async paginate(failures, maxInline, pdfOutputPath) {
13288
13298
  if (failures.length <= maxInline) {
@@ -13290,7 +13300,7 @@ var FailurePaginator = class {
13290
13300
  }
13291
13301
  const inline = failures.slice(0, maxInline);
13292
13302
  const overflow = failures.slice(maxInline);
13293
- const sidecarPath = this.sidecarPathFor(pdfOutputPath);
13303
+ const sidecarPath = sidecarPathFor(pdfOutputPath);
13294
13304
  try {
13295
13305
  await fs13.promises.writeFile(
13296
13306
  sidecarPath,
@@ -13298,7 +13308,7 @@ var FailurePaginator = class {
13298
13308
  "utf8"
13299
13309
  );
13300
13310
  logger.info(
13301
- `Emitted ${overflow.length} overflow failures \u2192 ${path13.basename(sidecarPath)}`
13311
+ `Emitted ${overflow.length} overflow failures \u2192 ${path12.basename(sidecarPath)}`
13302
13312
  );
13303
13313
  } catch (e) {
13304
13314
  logger.warn(`Could not write failure sidecar: ${e.message}`);
@@ -13306,11 +13316,6 @@ var FailurePaginator = class {
13306
13316
  }
13307
13317
  return { inline, overflowCount: overflow.length, sidecarPath };
13308
13318
  }
13309
- sidecarPathFor(pdfPath) {
13310
- const dir = path13.dirname(pdfPath);
13311
- const base = path13.basename(pdfPath, path13.extname(pdfPath));
13312
- return path13.join(dir, `${base}-failures.json`);
13313
- }
13314
13319
  /**
13315
13320
  * Drops the inlined image bytes — a 150-entry overflow would otherwise
13316
13321
  * produce a 40 MB JSON. The path still references the on-disk screenshot
@@ -13325,6 +13330,21 @@ var FailurePaginator = class {
13325
13330
  }
13326
13331
  };
13327
13332
 
13333
+ // src/pdf/sidecar-crypto.ts
13334
+ init_esm_shims();
13335
+ import crypto3 from "crypto";
13336
+ var MAGIC = Buffer.from("RFENC1");
13337
+ var SALT_LEN = 16;
13338
+ var IV_LEN = 12;
13339
+ function encryptSidecar(plain, password) {
13340
+ const salt = crypto3.randomBytes(SALT_LEN);
13341
+ const key = crypto3.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
13342
+ const iv = crypto3.randomBytes(IV_LEN);
13343
+ const cipher = crypto3.createCipheriv("aes-256-gcm", key, iv);
13344
+ const enc = Buffer.concat([cipher.update(plain), cipher.final()]);
13345
+ return Buffer.concat([MAGIC, salt, iv, cipher.getAuthTag(), enc]);
13346
+ }
13347
+
13328
13348
  // src/pdf/PdfGenerator.ts
13329
13349
  var PdfGenerator = class {
13330
13350
  constructor() {
@@ -13422,7 +13442,7 @@ var PdfGenerator = class {
13422
13442
  const templateId = source.kind === "builtin" ? source.id : "detailed";
13423
13443
  const templateData = { ...data, meta: { ...data.meta, template: templateId } };
13424
13444
  const templateOptions = { ...options, template: templateId };
13425
- await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
13445
+ let { sidecarPath } = await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
13426
13446
  const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
13427
13447
  if (capBytes > 0) {
13428
13448
  const sizeBytes = (await fs14.promises.stat(outputPath)).size;
@@ -13432,7 +13452,8 @@ var PdfGenerator = class {
13432
13452
  logger.warn(
13433
13453
  `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}).`
13434
13454
  );
13435
- await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
13455
+ const retuned = await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
13456
+ sidecarPath = retuned.sidecarPath ?? sidecarPath;
13436
13457
  const finalSize = (await fs14.promises.stat(outputPath)).size;
13437
13458
  if (finalSize > capBytes) {
13438
13459
  logger.warn(
@@ -13444,7 +13465,36 @@ var PdfGenerator = class {
13444
13465
  }
13445
13466
  }
13446
13467
  if (options.pdfPassword) {
13447
- await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
13468
+ let pdfEncrypted = false;
13469
+ try {
13470
+ await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
13471
+ pdfEncrypted = true;
13472
+ if (sidecarPath && fs14.existsSync(sidecarPath)) {
13473
+ const blob = encryptSidecar(await fs14.promises.readFile(sidecarPath), options.pdfPassword);
13474
+ await fs14.promises.writeFile(`${sidecarPath}.enc`, blob);
13475
+ await fs14.promises.unlink(sidecarPath);
13476
+ logger.info(`Overflow sidecar encrypted \u2192 ${path13.basename(sidecarPath)}.enc`);
13477
+ } else {
13478
+ const staleSidecarPath = sidecarPathFor(outputPath);
13479
+ if (fs14.existsSync(staleSidecarPath)) {
13480
+ fs14.rmSync(staleSidecarPath, { force: true });
13481
+ logger.info(`Removed stale plaintext sidecar: ${path13.basename(staleSidecarPath)}`);
13482
+ }
13483
+ }
13484
+ } catch (err) {
13485
+ const sidecarTargets = sidecarPath ? [sidecarPath, `${sidecarPath}.enc`] : [];
13486
+ const cleanupTargets = pdfEncrypted ? sidecarTargets : [outputPath, ...sidecarTargets];
13487
+ for (const target of cleanupTargets) {
13488
+ try {
13489
+ fs14.rmSync(target, { force: true });
13490
+ } catch (cleanupErr) {
13491
+ logger.warn(
13492
+ `Could not remove leftover file after encryption failure: ${target} (${cleanupErr.message})`
13493
+ );
13494
+ }
13495
+ }
13496
+ throw err;
13497
+ }
13448
13498
  }
13449
13499
  const finalSizeMb = fmtMb((await fs14.promises.stat(outputPath)).size);
13450
13500
  logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
@@ -13459,17 +13509,17 @@ var PdfGenerator = class {
13459
13509
  if (options.templatePath && options.templatePath.length > 0) {
13460
13510
  const byAbsolutePath = /* @__PURE__ */ new Map();
13461
13511
  for (const p of options.templatePath) {
13462
- const absolutePath = path14.isAbsolute(p) ? p : path14.resolve(process.cwd(), p);
13512
+ const absolutePath = path13.isAbsolute(p) ? p : path13.resolve(process.cwd(), p);
13463
13513
  if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
13464
13514
  }
13465
13515
  const labelCounts = /* @__PURE__ */ new Map();
13466
13516
  for (const rawPath of byAbsolutePath.values()) {
13467
- const label = path14.basename(rawPath, ".hbs");
13517
+ const label = path13.basename(rawPath, ".hbs");
13468
13518
  labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
13469
13519
  }
13470
13520
  const seenPerLabel = /* @__PURE__ */ new Map();
13471
13521
  return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
13472
- const baseLabel = path14.basename(rawPath, ".hbs");
13522
+ const baseLabel = path13.basename(rawPath, ".hbs");
13473
13523
  let label = baseLabel;
13474
13524
  if ((labelCounts.get(baseLabel) ?? 0) > 1) {
13475
13525
  const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
@@ -13529,7 +13579,7 @@ var PdfGenerator = class {
13529
13579
  ...data,
13530
13580
  failures: paginated.inline,
13531
13581
  overflowFailureCount: paginated.overflowCount,
13532
- overflowSidecarName: paginated.sidecarPath ? path14.basename(paginated.sidecarPath) : void 0
13582
+ overflowSidecarName: paginated.sidecarPath ? path13.basename(paginated.sidecarPath) + (options.pdfPassword ? ".enc" : "") : void 0
13533
13583
  };
13534
13584
  for (const f of renderData.failures) f.screenshotBase64 = void 0;
13535
13585
  if (options.includeScreenshots !== false) {
@@ -13554,7 +13604,7 @@ var PdfGenerator = class {
13554
13604
  await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
13555
13605
  const showCharts = source.kind === "builtin" ? resolveSections(source.id, options.sections).charts : true;
13556
13606
  await this.chartRenderer.waitForCharts(page, showCharts);
13557
- logger.info(`Generating PDF \u2192 ${path14.relative(process.cwd(), outputPath)}`);
13607
+ logger.info(`Generating PDF \u2192 ${path13.relative(process.cwd(), outputPath)}`);
13558
13608
  await page.pdf({
13559
13609
  path: outputPath,
13560
13610
  format: "A4",
@@ -13564,19 +13614,129 @@ var PdfGenerator = class {
13564
13614
  } finally {
13565
13615
  await this.htmlWriter.cleanup();
13566
13616
  }
13617
+ return { sidecarPath: paginated.overflowCount > 0 ? paginated.sidecarPath : void 0 };
13567
13618
  }
13568
13619
  };
13569
13620
  function fmtMb(bytes) {
13570
13621
  return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
13571
13622
  }
13572
13623
 
13624
+ // src/redact/Redactor.ts
13625
+ init_esm_shims();
13626
+ var Redactor = class {
13627
+ constructor(opts) {
13628
+ /** Indexes of user patterns disabled after throwing (warned once each). */
13629
+ this.disabled = /* @__PURE__ */ new Set();
13630
+ this.maskRaw = opts.mask;
13631
+ this.maskReplacement = opts.mask.replace(/\$/g, "$$$$");
13632
+ this.useBuiltins = opts.builtins;
13633
+ this.userPatterns = opts.patterns.map((p) => new RegExp(p, "g"));
13634
+ }
13635
+ redactText(s) {
13636
+ if (!s) return s;
13637
+ let out = s;
13638
+ if (this.useBuiltins) out = this.applyBuiltins(out);
13639
+ for (let i = 0; i < this.userPatterns.length; i++) {
13640
+ if (this.disabled.has(i)) continue;
13641
+ try {
13642
+ out = out.replace(this.userPatterns[i], this.maskReplacement);
13643
+ } catch (err) {
13644
+ this.disabled.add(i);
13645
+ logger.warn(
13646
+ `redact: custom pattern #${i + 1} threw and is disabled for this run: ${err.message}`
13647
+ );
13648
+ }
13649
+ }
13650
+ return out;
13651
+ }
13652
+ applyBuiltins(s) {
13653
+ const M = this.maskRaw;
13654
+ const MR = this.maskReplacement;
13655
+ return s.replace(/(\b[a-z][\w+.-]{0,63}:\/\/[^/\s:@]{1,512}:)([^@\s/]{1,512})(?=@)/gi, `$1${MR}`).replace(
13656
+ /\b(?:(authorization\s*[:=]\s*basic\s+)([A-Za-z0-9._~+/=-]{8,})|((?:authorization\s*[:=]\s*)?bearer\s+)([A-Za-z0-9._~+/=-]{8,}))/gi,
13657
+ (fullMatch, basicPrefix, _basicToken, bearerPrefix, bearerToken) => {
13658
+ if (basicPrefix) return `${basicPrefix}${M}`;
13659
+ if (bearerToken && /[^a-z]/.test(bearerToken)) return `${bearerPrefix}${M}`;
13660
+ return fullMatch;
13661
+ }
13662
+ ).replace(/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g, MR).replace(
13663
+ /\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{16,}|gho_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9_-]{16,}|rzp_(?:live|test)_[A-Za-z0-9]{8,}|AIza[A-Za-z0-9_-]{30,})\b/g,
13664
+ MR
13665
+ ).replace(
13666
+ /\b((?:[A-Za-z0-9]{1,32}[_-]){0,8}(?:password|passwd|passcode|pwd|secret|token|api[_-]?key|apikey|auth|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret))\b(["']?\s*[=:]\s*)(?!(?:bearer|basic)\b)(?:(["'])((?:(?!\3).){1,512})\3|([^\s"',;&]{1,512}))/gi,
13667
+ (_m, key, sep, q, _quotedVal, _plainVal) => q ? `${key}${sep}${q}${M}${q}` : `${key}${sep}${M}`
13668
+ ).replace(
13669
+ /\b(fill|type|press)\b([^\n]{0,80}?\b(?:password|secret|token|pass|passcode|pin|otp)\b[^\n]{0,80}?\s)(["'])((?:(?!\3).)+)\3/gi,
13670
+ (_m, action, mid, q) => `${action}${mid}${q}${M}${q}`
13671
+ ).replace(/\b([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]{0,63}@([A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,})\b/g, "$1***@$2").replace(/[A-Za-z0-9_+/=-]{20,}/g, (m, offset, str) => {
13672
+ const before = offset > 0 ? str[offset - 1] : " ";
13673
+ const after = offset + m.length < str.length ? str[offset + m.length] : " ";
13674
+ const beforeIsWord = /[A-Za-z0-9_]/.test(before);
13675
+ const afterIsWord = /[A-Za-z0-9_]/.test(after);
13676
+ if (!beforeIsWord && !afterIsWord && /\d/.test(m) && /[A-Za-z]/.test(m)) {
13677
+ return M;
13678
+ }
13679
+ return m;
13680
+ });
13681
+ }
13682
+ };
13683
+
13684
+ // src/redact/redact-report.ts
13685
+ init_esm_shims();
13686
+ function redactReportData(data, r) {
13687
+ const rd = (s) => s === void 0 ? void 0 : r.redactText(s);
13688
+ for (const f of data.failures) {
13689
+ f.testTitle = r.redactText(f.testTitle);
13690
+ f.suitePath = f.suitePath.map((s) => r.redactText(s));
13691
+ f.error.message = r.redactText(f.error.message);
13692
+ f.error.stack = rd(f.error.stack);
13693
+ if (f.execution) {
13694
+ f.execution.steps?.forEach((s) => redactStep(s, r));
13695
+ if (f.execution.failingStep) redactStep(f.execution.failingStep, r);
13696
+ if (f.execution.console) {
13697
+ f.execution.console.stdout = f.execution.console.stdout.map((l) => r.redactText(l));
13698
+ f.execution.console.stderr = f.execution.console.stderr.map((l) => r.redactText(l));
13699
+ }
13700
+ }
13701
+ }
13702
+ for (const p of data.projects) p.suites.forEach((s) => redactSuite(s, r));
13703
+ if (data.analysis) {
13704
+ for (const c of data.analysis.clusters) {
13705
+ c.representativeMessage = r.redactText(c.representativeMessage);
13706
+ c.tests = c.tests.map((t) => r.redactText(t));
13707
+ }
13708
+ data.analysis.oneLiner = r.redactText(data.analysis.oneLiner);
13709
+ }
13710
+ data.classifications?.forEach((c) => c.message = r.redactText(c.message));
13711
+ data.briefSentence = rd(data.briefSentence);
13712
+ data.charts.suiteResults.forEach((s) => s.label = r.redactText(s.label));
13713
+ data.charts.flakinessTable?.forEach((row) => row.title = r.redactText(row.title));
13714
+ data.environment.buildUrl = r.redactText(data.environment.buildUrl);
13715
+ }
13716
+ function redactStep(s, r) {
13717
+ s.title = r.redactText(s.title);
13718
+ if (s.errorMessage !== void 0) s.errorMessage = r.redactText(s.errorMessage);
13719
+ }
13720
+ function redactSuite(s, r) {
13721
+ s.title = r.redactText(s.title);
13722
+ for (const t of s.tests) {
13723
+ t.title = r.redactText(t.title);
13724
+ t.fullTitle = r.redactText(t.fullTitle);
13725
+ t.tags = t.tags.map((tag) => r.redactText(tag));
13726
+ t.annotations.forEach((a) => {
13727
+ if (a.description !== void 0) a.description = r.redactText(a.description);
13728
+ });
13729
+ }
13730
+ s.suites.forEach((child) => redactSuite(child, r));
13731
+ }
13732
+
13573
13733
  // src/reporter.ts
13574
- import path16 from "path";
13734
+ import path15 from "path";
13575
13735
 
13576
13736
  // src/history/HistoryManager.ts
13577
13737
  init_esm_shims();
13578
13738
  import fs15 from "fs";
13579
- import path15 from "path";
13739
+ import path14 from "path";
13580
13740
  var HistoryManager = class {
13581
13741
  constructor(filePath) {
13582
13742
  this.filePath = filePath;
@@ -13597,7 +13757,7 @@ var HistoryManager = class {
13597
13757
  }
13598
13758
  append(entry, maxSize) {
13599
13759
  try {
13600
- fs15.mkdirSync(path15.dirname(this.filePath), { recursive: true });
13760
+ fs15.mkdirSync(path14.dirname(this.filePath), { recursive: true });
13601
13761
  const next = {
13602
13762
  version: 1,
13603
13763
  entries: [entry, ...this.read()].slice(0, maxSize)
@@ -14100,6 +14260,10 @@ var LiveStreamer = class {
14100
14260
  }
14101
14261
  enqueueTest(update) {
14102
14262
  if (!this.enabled) return;
14263
+ if (this.config.redact) {
14264
+ if (update.title !== void 0) update.title = this.config.redact(update.title);
14265
+ if (update.currentStep !== void 0) update.currentStep = this.config.redact(update.currentStep);
14266
+ }
14103
14267
  const prevRetry = this.testStates.get(update.id)?.retry;
14104
14268
  this.testStates.set(update.id, { status: update.status, retry: update.retry ?? prevRetry });
14105
14269
  this.testBuffer.push(update);
@@ -14108,6 +14272,10 @@ var LiveStreamer = class {
14108
14272
  }
14109
14273
  enqueueEvent(event) {
14110
14274
  if (!this.enabled) return;
14275
+ if (this.config.redact) {
14276
+ if (event.title !== void 0) event.title = this.config.redact(event.title);
14277
+ if (event.text !== void 0) event.text = this.config.redact(event.text);
14278
+ }
14111
14279
  event.id = `${this.config.shardKey}:${this.eventSeq++}`;
14112
14280
  this.eventBuffer.push(event);
14113
14281
  this.capPreLicenseBuffer();
@@ -14414,7 +14582,10 @@ var PdfReporter = class {
14414
14582
  this.options = parseOptions(rawOptions);
14415
14583
  setLogLevel(this.options.logLevel);
14416
14584
  this.dataCollector = new DataCollector(this.options.capture);
14417
- this.version = true ? "0.22.1" : "0.x";
14585
+ if (this.options.redact.enabled) {
14586
+ this.redactor = new Redactor(this.options.redact);
14587
+ }
14588
+ this.version = true ? "0.23.0" : "0.x";
14418
14589
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14419
14590
  this.licenseClient = new LicenseClient({
14420
14591
  licenseKey: this.options.licenseKey,
@@ -14505,6 +14676,9 @@ var PdfReporter = class {
14505
14676
  const reportData = this._buildReportData(collected, licenseInfo);
14506
14677
  runFailureAnalysis(reportData, this.options, this.cwd);
14507
14678
  reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
14679
+ if (this.redactor) {
14680
+ redactReportData(reportData, this.redactor);
14681
+ }
14508
14682
  let pdfPaths = [];
14509
14683
  try {
14510
14684
  pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
@@ -14536,7 +14710,10 @@ var PdfReporter = class {
14536
14710
  const historyPath = resolveHistoryPath(this.options, this.cwd);
14537
14711
  const hm = new HistoryManager(historyPath);
14538
14712
  const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
14539
- const flakyTests = collectFlakyTests(collected.projects);
14713
+ const redactor = this.redactor;
14714
+ const flakyTests = collectFlakyTests(collected.projects).map(
14715
+ (t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
14716
+ );
14540
14717
  const entry = {
14541
14718
  runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
14542
14719
  timestamp: Date.now(),
@@ -14676,6 +14853,7 @@ var PdfReporter = class {
14676
14853
  const token = computeWatchToken(runId, this.options.licenseKey);
14677
14854
  const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
14678
14855
  const env = detectCiEnv();
14856
+ if (this.redactor && env?.buildUrl) env.buildUrl = this.redactor.redactText(env.buildUrl);
14679
14857
  if (watchUrl) {
14680
14858
  void this.announceLive(watchUrl, env, shard).catch(() => {
14681
14859
  });
@@ -14690,6 +14868,7 @@ var PdfReporter = class {
14690
14868
  shardKey: this.liveShardKey,
14691
14869
  shardTotal: shard?.total ?? null,
14692
14870
  license: () => this.license(),
14871
+ redact: this.redactor ? (s) => this.redactor.redactText(s) : void 0,
14693
14872
  env,
14694
14873
  flushMs: live.flushMs
14695
14874
  });
@@ -14731,7 +14910,7 @@ var PdfReporter = class {
14731
14910
  }
14732
14911
  resolveProjectName() {
14733
14912
  try {
14734
- const pkgPath = path16.resolve(process.cwd(), "package.json");
14913
+ const pkgPath = path15.resolve(process.cwd(), "package.json");
14735
14914
  const pkg = __require(pkgPath);
14736
14915
  return pkg.name ?? "Playwright Tests";
14737
14916
  } catch {
@@ -14781,9 +14960,9 @@ function deriveVerdict(stats) {
14781
14960
  }
14782
14961
  function resolveHistoryPath(options, cwd) {
14783
14962
  if (options.historyFile) {
14784
- return path16.isAbsolute(options.historyFile) ? options.historyFile : path16.resolve(cwd, options.historyFile);
14963
+ return path15.isAbsolute(options.historyFile) ? options.historyFile : path15.resolve(cwd, options.historyFile);
14785
14964
  }
14786
- return path16.join(resolveProjectDir(options, cwd), "history.json");
14965
+ return path15.join(resolveProjectDir(options, cwd), "history.json");
14787
14966
  }
14788
14967
  function flattenSuiteNodes(suites) {
14789
14968
  return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);