@reportforge/playwright-pdf 0.22.1 → 0.24.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)
@@ -14089,6 +14249,7 @@ var LiveStreamer = class {
14089
14249
  this.enabled = true;
14090
14250
  this.gated = false;
14091
14251
  this.gatePromise = null;
14252
+ this.announcePromise = null;
14092
14253
  this.jwt = null;
14093
14254
  this.envSent = false;
14094
14255
  this.lastWarnMs = 0;
@@ -14100,6 +14261,10 @@ var LiveStreamer = class {
14100
14261
  }
14101
14262
  enqueueTest(update) {
14102
14263
  if (!this.enabled) return;
14264
+ if (this.config.redact) {
14265
+ if (update.title !== void 0) update.title = this.config.redact(update.title);
14266
+ if (update.currentStep !== void 0) update.currentStep = this.config.redact(update.currentStep);
14267
+ }
14103
14268
  const prevRetry = this.testStates.get(update.id)?.retry;
14104
14269
  this.testStates.set(update.id, { status: update.status, retry: update.retry ?? prevRetry });
14105
14270
  this.testBuffer.push(update);
@@ -14108,11 +14273,67 @@ var LiveStreamer = class {
14108
14273
  }
14109
14274
  enqueueEvent(event) {
14110
14275
  if (!this.enabled) return;
14276
+ if (this.config.redact) {
14277
+ if (event.title !== void 0) event.title = this.config.redact(event.title);
14278
+ if (event.text !== void 0) event.text = this.config.redact(event.text);
14279
+ }
14111
14280
  event.id = `${this.config.shardKey}:${this.eventSeq++}`;
14112
14281
  this.eventBuffer.push(event);
14113
14282
  this.capPreLicenseBuffer();
14114
14283
  this.scheduleFlush();
14115
14284
  }
14285
+ /**
14286
+ * One immediate empty "hello" batch, sent at run start before the banner
14287
+ * prints. It binds the run server-side (so early link-openers and the
14288
+ * dashboard see it instantly instead of a pending window) and returns the
14289
+ * server-ISSUED watch token from the ack — the capability that survives
14290
+ * every license-key lifecycle event, unlike the key-derived HMAC fallback.
14291
+ * Returns null on any failure (not entitled, network error, older server
14292
+ * whose ack carries no token); the caller then falls back to the HMAC link.
14293
+ * Memoized so drain() can await the same in-flight promise (see drain()).
14294
+ */
14295
+ announce() {
14296
+ return this.announcePromise ?? (this.announcePromise = this.announceOnce());
14297
+ }
14298
+ async announceOnce() {
14299
+ const entitled = await this.ensureGate();
14300
+ if (!entitled || !this.jwt) return null;
14301
+ const body = {
14302
+ runId: this.config.runId,
14303
+ shardKey: this.config.shardKey,
14304
+ shardTotal: this.config.shardTotal,
14305
+ status: "running",
14306
+ counts: this.snapshotCounts(),
14307
+ tests: [],
14308
+ events: [],
14309
+ ...this.envSent ? {} : { env: this.config.env }
14310
+ };
14311
+ const controller = new AbortController();
14312
+ const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS3);
14313
+ try {
14314
+ const res = await fetch(`${this.serverUrl}/api/live/ingest`, {
14315
+ method: "POST",
14316
+ headers: {
14317
+ "content-type": "application/json",
14318
+ authorization: `Bearer ${this.jwt}`
14319
+ },
14320
+ body: JSON.stringify(body),
14321
+ signal: controller.signal
14322
+ });
14323
+ if (!res.ok) {
14324
+ await res.body?.cancel().catch(() => {
14325
+ });
14326
+ return null;
14327
+ }
14328
+ const ack = await res.json().catch(() => null);
14329
+ if (body.env) this.envSent = true;
14330
+ return typeof ack?.watchToken === "string" && ack.watchToken ? ack.watchToken : null;
14331
+ } catch {
14332
+ return null;
14333
+ } finally {
14334
+ clearTimeout(timeout);
14335
+ }
14336
+ }
14116
14337
  /**
14117
14338
  * Final flush: cancels the pending timer, emits the terminal `finished` batch,
14118
14339
  * then awaits in-flight requests bounded by LIVE_DRAIN_DEADLINE_MS. Called from
@@ -14124,6 +14345,7 @@ var LiveStreamer = class {
14124
14345
  clearTimeout(this.flushTimer);
14125
14346
  this.flushTimer = null;
14126
14347
  }
14348
+ if (this.announcePromise) await this.announcePromise;
14127
14349
  await this.flush("finished");
14128
14350
  await Promise.race([
14129
14351
  Promise.allSettled([...this.inFlight]),
@@ -14301,7 +14523,7 @@ function computeWatchToken(runId, key) {
14301
14523
  }
14302
14524
  function buildWatchUrl(serverUrl, runId, token) {
14303
14525
  const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
14304
- return `${base}/live/${encodeURIComponent(runId)}?t=${encodeURIComponent(token)}`;
14526
+ return `${base}/live/${encodeURIComponent(runId)}#t=${encodeURIComponent(token)}`;
14305
14527
  }
14306
14528
  function formatLiveBanner(url) {
14307
14529
  const label = "Live Tracker";
@@ -14414,7 +14636,10 @@ var PdfReporter = class {
14414
14636
  this.options = parseOptions(rawOptions);
14415
14637
  setLogLevel(this.options.logLevel);
14416
14638
  this.dataCollector = new DataCollector(this.options.capture);
14417
- this.version = true ? "0.22.1" : "0.x";
14639
+ if (this.options.redact.enabled) {
14640
+ this.redactor = new Redactor(this.options.redact);
14641
+ }
14642
+ this.version = true ? "0.24.0" : "0.x";
14418
14643
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14419
14644
  this.licenseClient = new LicenseClient({
14420
14645
  licenseKey: this.options.licenseKey,
@@ -14505,6 +14730,9 @@ var PdfReporter = class {
14505
14730
  const reportData = this._buildReportData(collected, licenseInfo);
14506
14731
  runFailureAnalysis(reportData, this.options, this.cwd);
14507
14732
  reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
14733
+ if (this.redactor) {
14734
+ redactReportData(reportData, this.redactor);
14735
+ }
14508
14736
  let pdfPaths = [];
14509
14737
  try {
14510
14738
  pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
@@ -14536,7 +14764,10 @@ var PdfReporter = class {
14536
14764
  const historyPath = resolveHistoryPath(this.options, this.cwd);
14537
14765
  const hm = new HistoryManager(historyPath);
14538
14766
  const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
14539
- const flakyTests = collectFlakyTests(collected.projects);
14767
+ const redactor = this.redactor;
14768
+ const flakyTests = collectFlakyTests(collected.projects).map(
14769
+ (t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
14770
+ );
14540
14771
  const entry = {
14541
14772
  runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
14542
14773
  timestamp: Date.now(),
@@ -14673,36 +14904,39 @@ var PdfReporter = class {
14673
14904
  this.liveSteps = live.steps;
14674
14905
  this.liveConsole = live.console;
14675
14906
  void this.license();
14676
- const token = computeWatchToken(runId, this.options.licenseKey);
14677
- const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
14678
14907
  const env = detectCiEnv();
14679
- if (watchUrl) {
14680
- void this.announceLive(watchUrl, env, shard).catch(() => {
14681
- });
14682
- } else {
14683
- logger.warn(
14684
- "Live is enabled but no RF_LICENSE_KEY was found \u2014 cannot mint a watch link or stream. Set RF_LICENSE_KEY (or the `licenseKey` option)."
14685
- );
14686
- }
14908
+ if (this.redactor && env?.buildUrl) env.buildUrl = this.redactor.redactText(env.buildUrl);
14687
14909
  this.liveStreamer = new LiveStreamer({
14688
14910
  serverUrl,
14689
14911
  runId,
14690
14912
  shardKey: this.liveShardKey,
14691
14913
  shardTotal: shard?.total ?? null,
14692
14914
  license: () => this.license(),
14915
+ redact: this.redactor ? (s) => this.redactor.redactText(s) : void 0,
14693
14916
  env,
14694
14917
  flushMs: live.flushMs
14695
14918
  });
14919
+ if (!this.options.licenseKey?.trim() && !process.env.RF_LICENSE_KEY?.trim()) {
14920
+ logger.warn(
14921
+ "Live is enabled but no RF_LICENSE_KEY was found \u2014 cannot mint a watch link or stream. Set RF_LICENSE_KEY (or the `licenseKey` option)."
14922
+ );
14923
+ return;
14924
+ }
14925
+ void this.announceLive(serverUrl, runId, env, shard).catch(() => {
14926
+ });
14696
14927
  }
14697
14928
  /**
14698
14929
  * Announces the watch link once the run is confirmed live-entitled. Gating on
14699
14930
  * the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
14700
14931
  * feature) guarantees the link is never printed or posted for a run that will
14701
- * never stream — no dead links. The chat ping fires from the primary shard only
14702
- * (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
14703
- * not N. The banner still prints from every shard's own log.
14932
+ * never stream — no dead links. The link's token is the server-ISSUED one from
14933
+ * the announce handshake (random, key-independent a key rotation can never
14934
+ * kill the link); the key-derived HMAC is minted only as a fallback when the
14935
+ * handshake fails (network blip, older server). The chat ping fires from the
14936
+ * primary shard only (unsharded, or `shard.current === 1`) so an N-way matrix
14937
+ * posts one message, not N. The banner still prints from every shard's own log.
14704
14938
  */
14705
- async announceLive(watchUrl, env, shard) {
14939
+ async announceLive(serverUrl, runId, env, shard) {
14706
14940
  const info = await this.license();
14707
14941
  if (!info) {
14708
14942
  logger.warn("Live is enabled but no active license resolved \u2014 no watch link or streaming.");
@@ -14720,6 +14954,13 @@ var PdfReporter = class {
14720
14954
  );
14721
14955
  return;
14722
14956
  }
14957
+ const issued = await this.liveStreamer?.announce();
14958
+ const token = issued ?? computeWatchToken(runId, this.options.licenseKey);
14959
+ if (!token) {
14960
+ logger.warn("Live is enabled but no watch link could be established \u2014 streaming continues without one.");
14961
+ return;
14962
+ }
14963
+ const watchUrl = buildWatchUrl(serverUrl, runId, token);
14723
14964
  if (isLevelEnabled("info")) {
14724
14965
  console.log(formatLiveBanner(watchUrl));
14725
14966
  }
@@ -14731,7 +14972,7 @@ var PdfReporter = class {
14731
14972
  }
14732
14973
  resolveProjectName() {
14733
14974
  try {
14734
- const pkgPath = path16.resolve(process.cwd(), "package.json");
14975
+ const pkgPath = path15.resolve(process.cwd(), "package.json");
14735
14976
  const pkg = __require(pkgPath);
14736
14977
  return pkg.name ?? "Playwright Tests";
14737
14978
  } catch {
@@ -14781,9 +15022,9 @@ function deriveVerdict(stats) {
14781
15022
  }
14782
15023
  function resolveHistoryPath(options, cwd) {
14783
15024
  if (options.historyFile) {
14784
- return path16.isAbsolute(options.historyFile) ? options.historyFile : path16.resolve(cwd, options.historyFile);
15025
+ return path15.isAbsolute(options.historyFile) ? options.historyFile : path15.resolve(cwd, options.historyFile);
14785
15026
  }
14786
- return path16.join(resolveProjectDir(options, cwd), "history.json");
15027
+ return path15.join(resolveProjectDir(options, cwd), "history.json");
14787
15028
  }
14788
15029
  function flattenSuiteNodes(suites) {
14789
15030
  return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);