ticketlens 0.38.36 → 0.38.37

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
@@ -7,7 +7,7 @@
7
7
  <img src="https://img.shields.io/npm/dm/ticketlens?style=flat-square&color=06b6d4&label=downloads" />
8
8
  <img src="https://github.com/ralphmoran/ticket-lens/actions/workflows/test.yml/badge.svg?style=flat-square" />
9
9
  <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" />
10
- <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen?style=flat-square" />
10
+ <img src="https://img.shields.io/badge/node-%3E%3D22-brightgreen?style=flat-square" />
11
11
  </div>
12
12
 
13
13
  </div>
@@ -100,7 +100,7 @@ npx ticketlens CNV1-2
100
100
 
101
101
  Tip: `tl` works everywhere `ticketlens` does — running `tl`/`ticketlens config` before anything is configured also launches guided setup, no dead end. Pass `--no-input` to force non-interactive behavior even in a terminal (scripts, CI).
102
102
 
103
- **Prerequisites:** Node.js >=20
103
+ **Prerequisites:** Node.js >=22.6
104
104
 
105
105
  ---
106
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.38.36",
3
+ "version": "0.38.37",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,6 +40,6 @@
40
40
  },
41
41
  "homepage": "https://github.com/ralphmoran/ticket-lens",
42
42
  "engines": {
43
- "node": ">=20.0.0"
43
+ "node": ">=22.6.0"
44
44
  }
45
45
  }
@@ -3,7 +3,7 @@
3
3
  * Centralised here to avoid triplicating the regex and warning logic.
4
4
  */
5
5
 
6
- export const DEFAULT_API_BASE = 'http://api.ticketlens.test';
6
+ export const DEFAULT_API_BASE = 'https://api.ticketlens.app';
7
7
  export const DEFAULT_SITE_BASE = 'https://ticketlens.app';
8
8
 
9
9
  // Matches localhost, 127.0.0.1, and any hostname ending in .test or .local,
@@ -32,6 +32,30 @@ const MAX_JOINED_CHUNKS = 4;
32
32
  const MAX_COMPOUND_SEGMENT_LENGTH = 15;
33
33
  const HYPHENATED_COMPOUND_RE = /^[A-Za-z]+(-[A-Za-z]+)+$/;
34
34
 
35
+ // A candidate containing an unstripped '(', ')', '[', or ']' reads as code
36
+ // syntax (an array/list literal element, or a function-call argument — e.g.
37
+ // "['compliance', '--help']" or "matches(x)") rather than a secret fragment.
38
+ // Consulted ONLY from looksLikeCodeSyntax below, which downgrades a matching
39
+ // high-entropy candidate to a warning — deliberately NOT wired into
40
+ // isLabelWord (backlog #14 residual, code review caught this on the first
41
+ // pass): making a bracket-bearing token a hard isLabelWord stop would end a
42
+ // joinedChunkRuns run there unconditionally, the same way GIT_REFERENCE_WORD_RE/
43
+ // isHyphenatedWordCompound/looksLikeFilenameReference already do — but unlike
44
+ // those, a bracket character is trivial for an attacker to insert anywhere
45
+ // ("a(b"), and doing so would fully and SILENTLY stop a genuine fragmented
46
+ // secret split around it from ever being reassembled for the entropy check
47
+ // (confirmed live: a real 36-char secret split into two 18-char halves around
48
+ // a bare "a(b" separator went from rejected:true to a fully silent
49
+ // rejected:false/warnings:[] once isLabelWord treated brackets as a stop).
50
+ // The downgrade-only design below avoids that: the join still happens (a
51
+ // bracket-bearing token is ordinary, never a label word), so any joined
52
+ // candidate spanning real secret content still trips the entropy check —
53
+ // and since that joined candidate necessarily still contains the bracket
54
+ // character too, looksLikeCodeSyntax downgrades it to a WARNING rather than
55
+ // silently exempting it, unlike the fully-silent gap the hard-wall version
56
+ // would have reopened.
57
+ const CODE_SYNTAX_RE = /[()[\]]/;
58
+
35
59
  // U+200B (ZERO WIDTH SPACE) is added explicitly: despite the name, it does
36
60
  // NOT carry the Unicode White_Space property (General_Category=Cf, not Zs),
37
61
  // so it's excluded from JS's native \s (ECMA-262 WhiteSpace production) —
@@ -147,6 +171,29 @@ function looksLikeFilenameReference(strippedToken) {
147
171
  return FILENAME_REFERENCE_RE.test(strippedToken);
148
172
  }
149
173
 
174
+ /**
175
+ * True for a candidate — a raw token OR a joinedChunkRuns result — that
176
+ * itself contains an unstripped '(', ')', '[', or ']': code syntax (a
177
+ * function-call argument or array/list-literal element) rather than a secret
178
+ * fragment. Deliberately NOT wired into isLabelWord/joinedChunkRuns (see
179
+ * CODE_SYNTAX_RE's own comment for why that hard-wall approach reopened a
180
+ * silent reassembly bypass) — instead this only downgrades a candidate that
181
+ * ALREADY tripped looksRandom, same never-exempt treatment as
182
+ * looksLikeCodeFilename below. Covers both backlog #14 residual shapes: a
183
+ * token already 20+ chars on its own with no join needed (e.g.
184
+ * "matches(['compliance'," — whitespace-split with no space after '(' or
185
+ * '[', so it's one raw token from the very first split), and a joined run
186
+ * that only crosses the entropy threshold once several bracket-literal
187
+ * tokens glue together (e.g. "['compliance','--help','-h','debug']") — the
188
+ * bracket character survives into the joined string either way, so this
189
+ * still catches it. Fully exempting this shape would let a 20+ char secret
190
+ * dodge rejection just by wrapping it in a fake "f(" / "[" — see the
191
+ * security regression test alongside looksLikeCodeFilename's.
192
+ */
193
+ function looksLikeCodeSyntax(rawToken) {
194
+ return CODE_SYNTAX_RE.test(stripEdgePunctuation(rawToken));
195
+ }
196
+
150
197
  function shannonEntropy(token) {
151
198
  const counts = new Map();
152
199
  for (const ch of token) counts.set(ch, (counts.get(ch) ?? 0) + 1);
@@ -396,12 +443,15 @@ export function scanForSecrets({ title = '', tags = [], body = '' } = {}) {
396
443
  // Downgrading to a warning — never silently dropping the signal — matches
397
444
  // how an email address is already handled below.
398
445
  const randomCandidates = candidates.filter(token => !EMAIL_RE.test(token) && looksRandom(token, combined));
399
- if (randomCandidates.some(token => !looksLikeCodeFilename(token))) {
446
+ if (randomCandidates.some(token => !looksLikeCodeFilename(token) && !looksLikeCodeSyntax(token))) {
400
447
  reasons.push('Contains a long, random-looking string that could be a secret.');
401
448
  }
402
449
  if (randomCandidates.some(token => looksLikeCodeFilename(token))) {
403
450
  warnings.push('Contains a code-filename-shaped token that also reads as high-entropy — double-check it is not a credential.');
404
451
  }
452
+ if (randomCandidates.some(token => looksLikeCodeSyntax(token))) {
453
+ warnings.push('Contains a code-syntax-shaped token (brackets or parentheses) that also reads as high-entropy — double-check it is not a credential.');
454
+ }
405
455
 
406
456
  if (EMAIL_RE.test(combined)) {
407
457
  warnings.push('Contains an email address.');