reporting-labs 0.6.2 → 0.6.3

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/README.md CHANGED
@@ -134,7 +134,7 @@ await testData(rowsFromExcelOrJson, 'Coupons'); // arr
134
134
  await testData(fs.readFileSync('data/users.csv', 'utf8'), 'users.csv'); // CSV text → table
135
135
  ```
136
136
 
137
- **Secrets are masked automatically.** Passwords, tokens, API keys, `Authorization` and `Cookie` headers, JWTs and `Bearer ...` values show as `****`. Add your own keys with `maskKeys: ['otp', 'pan']` in the config.
137
+ **Secrets are masked automatically**, wherever they show up: `console.log` output, `log()` lines, `testData()` blocks, API headers and bodies, step titles and assertion messages. Covered forms: `password=x`, `Password: x`, `{ password: 'x' }`, `"password":"x"`, `X-Api-Key: x`, `access_token=x`, "password is x", "with password S3cret@1", `Bearer ...` / `Basic ...`, JWTs, and well-known token formats (GitHub, AWS, Slack, Stripe, Google, GitLab, npm, SendGrid). A failing `expect(token)` shows `Received: "****"`. Add your own keys with `maskKeys: ['otp', 'pan']` in the config; they apply to text too.
138
138
 
139
139
  ## API calls: recorded on their own
140
140
 
package/dist/mask.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export declare function makeMasker(extraKeys?: string[]): {
2
2
  mask: (v: unknown, key?: string) => unknown;
3
- maskStr: (s: string) => string;
3
+ maskStr: (input: string) => string;
4
4
  isSensitive: (k: string) => boolean;
5
5
  };
6
6
  export declare function parseCsv(text: string): {
package/dist/mask.js CHANGED
@@ -3,20 +3,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.makeMasker = makeMasker;
4
4
  exports.parseCsv = parseCsv;
5
5
  const DEFAULT_KEYS = ['password', 'passwd', 'pwd', 'secret', 'token', 'apikey', 'api_key', 'api-key', 'authorization', 'auth', 'cookie', 'set-cookie', 'session', 'credential', 'private', 'ssn', 'cvv', 'card'];
6
+ /** Values that are secrets on their own, whatever the surrounding text. */
6
7
  const VALUE_PATTERNS = [
7
- /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+\/=-]{8,}/gi,
8
+ /\b(Bearer|Basic|Digest|Token)\s+[A-Za-z0-9._~+\/=-]{8,}/gi,
8
9
  /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/g, // JWT
9
- /\b(sk|pk|ghp|xox[abp]|AKIA)[_-]?[A-Za-z0-9]{12,}\b/g,
10
- /(password|passwd|pwd|token|secret|api[_-]?key)(\s*[=:]\s*)([^\s&;,"']+)/gi,
10
+ /\b(?:sk|pk|rk)[-_](?:live|test)?[-_]?[A-Za-z0-9]{12,}\b/g, // Stripe-style keys
11
+ /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g, // GitHub tokens
12
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
13
+ /\bxox[abpr]-[A-Za-z0-9-]{10,}\b/g, // Slack
14
+ /\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id
15
+ /\bAIza[0-9A-Za-z_-]{30,}\b/g, // Google API key
16
+ /\bya29\.[0-9A-Za-z_-]{20,}\b/g, // Google OAuth token
17
+ /\bglpat-[A-Za-z0-9_-]{20,}\b/g, // GitLab
18
+ /\bnpm_[A-Za-z0-9]{30,}\b/g, // npm
19
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, // SendGrid
11
20
  ];
21
+ /** Words after "password is ..." that are plainly not a secret. */
22
+ const NOT_A_SECRET = new Set(['valid', 'invalid', 'visible', 'hidden', 'required', 'optional', 'missing', 'empty', 'null', 'undefined', 'none',
23
+ 'correct', 'incorrect', 'wrong', 'right', 'expired', 'set', 'unset', 'not', 'present', 'absent', 'ok', 'true', 'false', 'masked', 'blank',
24
+ 'too', 'the', 'a', 'an', 'same', 'different', 'changed', 'unchanged', 'shown', 'displayed', 'accepted', 'rejected', 'weak', 'strong']);
25
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
26
  function makeMasker(extraKeys = []) {
13
27
  const keys = [...DEFAULT_KEYS, ...extraKeys.map(k => k.toLowerCase())];
14
28
  const isSensitive = (k) => { const l = k.toLowerCase(); return keys.some(s => l === s || l.includes(s)); };
15
- const maskStr = (s) => VALUE_PATTERNS.reduce((acc, re) => acc.replace(re, (m, ...g) => {
16
- if (g.length >= 3 && typeof g[1] === 'string' && /[=:]/.test(g[1]))
17
- return g[0] + g[1] + '****';
18
- return m.split(/\s+/)[0] + ' ****';
19
- }), s);
29
+ // Key names as they appear in free text: password, X-Api-Key, access_token, userPassword, "password", db.password...
30
+ // `auth`, `otp`, `pin`... are matched as whole words only, so "author" or "pinned" stay untouched.
31
+ const compound = ['password', 'passwd', 'pwd', 'passcode', 'secret', 'token', 'api[_-]?key', 'cookie', 'credentials?', 'private[_-]?key', 'access[_-]?key', 'session[_-]?id', ...extraKeys.map(esc)];
32
+ const exact = ['authorization', 'auth', 'otp', 'pin', 'cvv', 'ssn'];
33
+ const KEY = `(?:[\\w.-]*?(?:${compound.join('|')})[\\w-]*|${exact.join('|')})`;
34
+ // key = value | key: value | "key": "value" | key => value | key -> value (value runs to a delimiter; a closing quote stays)
35
+ const KV = new RegExp(`(["']?)\\b(${KEY})\\b(["']?)(\\s*(?:=>|->|[=:])\\s*)(["']?)([^"'\\s&;,}\\]\\)]+)`, 'gi');
36
+ // password is S3cret | token was abc | password 'S3cret' | with password S3cret@123 (only when the value looks like a secret)
37
+ const SPOKEN_KEYS = ['password', 'passwd', 'pwd', 'passcode', 'secret', 'token', 'api[ _-]?key', 'access[ _-]?key', 'otp', 'pin', 'cvv', ...extraKeys.map(esc)];
38
+ const SPOKEN = new RegExp(`\\b(${SPOKEN_KEYS.join('|')})(s?\\b\\s+(?:is|was|of|as)?\\s*['"]?)([^\\s'",;]+)`, 'gi');
39
+ const looksSecret = (v) => !/\s/.test(v) && (/\d/.test(v) && v.length >= 4 || /[^A-Za-z0-9]/.test(v) && v.length >= 6 || /[a-z][A-Z]|[A-Z][a-z].*[A-Z]/.test(v) && v.length >= 8);
40
+ // Assertion output: when the text talks about a password/token/secret, the compared values are that secret.
41
+ // expect(token, 'token should match').toBe('x') -> Expected: "x" / Received: "tok_9f8e..."
42
+ const MENTIONS_SECRET = new RegExp(`\\b(?:${SPOKEN_KEYS.join('|')})s?\\b`, 'i');
43
+ const COMPARED = /\b(Expected|Received|Actual|expected|received|actual|got)(\s*[:=]\s*)(["'\u201c]?)([^"'\u201d\s]+)/g;
44
+ const maskStr = (input) => {
45
+ let s = input;
46
+ for (const re of VALUE_PATTERNS)
47
+ s = s.replace(re, m => /^(Bearer|Basic|Digest|Token)\s/i.test(m) ? m.split(/\s+/)[0] + ' ****' : '****');
48
+ s = s.replace(KV, (_m, q1, key, q2, sep, q3, _value) => `${q1}${key}${q2}${sep}${q3}****`);
49
+ s = s.replace(SPOKEN, (m, key, between, value) => {
50
+ const word = value.toLowerCase().replace(/[^a-z0-9]/g, '');
51
+ if (NOT_A_SECRET.has(word) || word === '****' || value === '****')
52
+ return m;
53
+ const explicit = /\b(is|was|of|as)\b|['"]$/.test(between);
54
+ return explicit || looksSecret(value) ? `${key}${between}****` : m;
55
+ });
56
+ if (MENTIONS_SECRET.test(s))
57
+ s = s.replace(COMPARED, (m, label, sep, q, value) => looksSecret(value) ? `${label}${sep}${q}****` : m);
58
+ return s;
59
+ };
20
60
  const mask = (v, key = '') => {
21
61
  if (key && isSensitive(key))
22
62
  return '****';
package/dist/reporter.js CHANGED
@@ -437,14 +437,15 @@ class ReportingLabsReporter {
437
437
  return path.relative(base, file).split(path.sep).join('/');
438
438
  }
439
439
  serializeError(e) {
440
- const out = { message: stripAnsi(e.message ?? e.value ?? '') };
440
+ // Assertions print received/expected values, so a failing `expect(token)` would leak the token without this.
441
+ const out = { message: this.masker.maskStr(stripAnsi(e.message ?? e.value ?? '')) };
441
442
  const why = (0, explain_1.explainError)(out.message);
442
443
  if (why)
443
444
  out.explain = why;
444
445
  if (e.stack)
445
- out.stack = stripAnsi(e.stack);
446
+ out.stack = this.masker.maskStr(stripAnsi(e.stack));
446
447
  if (e.snippet)
447
- out.snippet = stripAnsi(e.snippet);
448
+ out.snippet = this.masker.maskStr(stripAnsi(e.snippet));
448
449
  if (e.location)
449
450
  out.location = { file: this.rel(e.location.file), line: e.location.line, column: e.location.column };
450
451
  return out;
@@ -538,10 +539,10 @@ class ReportingLabsReporter {
538
539
  }
539
540
  serializeStep(s) {
540
541
  return {
541
- title: s.title,
542
+ title: this.masker.maskStr(s.title),
542
543
  category: s.category,
543
544
  duration: s.duration,
544
- error: s.error?.message ? stripAnsi(s.error.message) : undefined,
545
+ error: s.error?.message ? this.masker.maskStr(stripAnsi(s.error.message)) : undefined,
545
546
  steps: s.steps.filter(c => !isInternalAttach(c)).map(c => this.serializeStep(c)),
546
547
  };
547
548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reporting-labs",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "reportingLabs – beautiful, customizable single-file HTML test reports. One line of config. Playwright adapter included, more runners on the way.",
5
5
  "keywords": [
6
6
  "test-report",