pi-roundtable 0.7.6 → 0.7.8

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/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.7.8] - 2026-10-03
9
+
10
+ ### Fixed
11
+
12
+ - `scrubDiagnostic` masks a value to its end when an earlier rule had masked only its head (`password=sk-…!tail`, `GITHUB_TOKEN=ghp_…!tail`, a quoted value that continues after a token shape). A `[redacted]` an earlier rule wrote is read as part of an unquoted value, so `password=https://user:pw@host/x` ends as `password=[redacted]` with no `]]`.
13
+
14
+ ## [0.7.7] - 2026-10-03
15
+
16
+ ### Changed
17
+
18
+ - `scrubDiagnostic` takes linear time on a long run of `eyJ-` too: the JWT pattern now starts at the beginning of a run (a JWT glued directly after `-` is no longer masked by that pattern, only as the value of a secret-named field).
19
+ - `scrubDiagnostic` reads a quoted value up to its closing quote, so a secret holding `}`, `]`, blanks, `&`, `,` or `;` is masked whole; `}` and `]` end only an unquoted value. A value an earlier rule already masked is not masked again (no `[redacted]]`).
20
+
8
21
  ## [0.7.6] - 2026-10-03
9
22
 
10
23
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "A plugin-driven Pi agent server for Discord",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,9 @@
1
1
  /*
2
- * Every pattern here either starts with a literal or starts at the beginning of a run of its own
3
- * character class (a lookbehind, never a leading `\b` before a repeatable class), so scrubbing
4
- * takes time linear in the text at any length.
2
+ * A pattern whose first characters can recur inside the run it then scans (a leading `\b` before a
3
+ * repeatable class, or a literal such as `eyJ` that may appear again after a `-`) restarts at every
4
+ * repeat and takes quadratic time. Those patterns start at the beginning of a run of their own
5
+ * character class through a lookbehind; the others begin with a literal that cannot recur inside
6
+ * what they scan. Scrubbing then takes time linear in the text at any length.
5
7
  */
6
8
 
7
9
  /** Tool text that may name a credential: userinfo in URLs, bearer values, and well-known token shapes. */
@@ -24,7 +26,10 @@ const CREDENTIALS: [RegExp, string | ((...match: string[]) => string)][] = [
24
26
  /\b(cookie|set-cookie|x-api-key|x-auth-token|x-access-token)\s*:\s*[^\r\n]+/gi,
25
27
  "$1: [redacted]",
26
28
  ],
27
- [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g, "[redacted]"],
29
+ [
30
+ /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
31
+ "[redacted]",
32
+ ],
28
33
  [
29
34
  /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
30
35
  "[redacted]",
@@ -56,7 +61,29 @@ function secretKind(name: string): "secret" | "token" | undefined {
56
61
 
57
62
  /** The name and separator of an assignment, query parameter or JSON field; the value is read separately. */
58
63
  const ASSIGNMENT = /(?<![a-z0-9_-])([a-z0-9_-]+)(["']?\s*[:=]\s*["']?)/gi;
59
- const VALUE = /[^\s"',&;}\]]+/y;
64
+ /**
65
+ * An unquoted value ends at a blank, a quote, a separator or a closing bracket, except that a
66
+ * `[redacted]` an earlier rule wrote is part of the value, so what follows it is read too.
67
+ */
68
+ const UNQUOTED = /(?:\[redacted\]|[^\s"',&;}\]])+/y;
69
+ /** A quoted value runs to its closing quote, whatever it holds. */
70
+ const DOUBLE_QUOTED = /[^"]+/y;
71
+ const SINGLE_QUOTED = /[^']+/y;
72
+
73
+ /** The value that follows a separator ending in `end`, or undefined when there is none. */
74
+ function valueAt(
75
+ text: string,
76
+ end: number,
77
+ separator: string,
78
+ ): string | undefined {
79
+ const quote = separator.endsWith('"')
80
+ ? DOUBLE_QUOTED
81
+ : separator.endsWith("'")
82
+ ? SINGLE_QUOTED
83
+ : UNQUOTED;
84
+ quote.lastIndex = end;
85
+ return quote.exec(text)?.[0];
86
+ }
60
87
 
61
88
  /** Masks the value of every assignment whose name says it is a secret, in any case. */
62
89
  function maskAssignments(text: string): string {
@@ -69,10 +96,10 @@ function maskAssignments(text: string): string {
69
96
  // The value is read only for a secret name, so ordinary text costs nothing more to pass.
70
97
  const kind = secretKind(match[1] ?? "");
71
98
  if (!kind) continue;
72
- VALUE.lastIndex = end;
73
- const value = VALUE.exec(text)?.[0];
74
- if (value === undefined || (kind === "token" && /^[0-9]+$/.test(value)))
75
- continue;
99
+ const value = valueAt(text, end, match[2] ?? "");
100
+ // An earlier rule already masked this whole value; if it masked only the head, the rest goes too.
101
+ if (value === undefined || value === "[redacted]") continue;
102
+ if (kind === "token" && /^[0-9]+$/.test(value)) continue;
76
103
  out += `${text.slice(cursor, end)}[redacted]`;
77
104
  cursor = end + value.length;
78
105
  }