ticketlens 0.38.23 → 0.38.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.38.23",
3
+ "version": "0.38.24",
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": {
@@ -156,6 +156,7 @@ const TOOLS = [
156
156
  summary: { type: 'string', description: 'Ticket title/summary.' },
157
157
  description: { type: 'string', description: 'Ticket description. Omit for none.' },
158
158
  attachments: { type: 'array', items: { type: 'string' }, description: 'Local file paths to attach, uploaded after the ticket is created. On Linear the image is automatically linked into the description. On Jira it becomes a real, visible attachment on the issue, but is not embedded inline in the initial description (use ticket_comment afterward for an inline thumbnail). Not supported on GitHub.' },
159
+ profile: { type: 'string', description: 'Connection profile to target, overriding folder-based inference and the default profile. Use this when `project` belongs to a profile other than the one auto-resolved from the current working directory.' },
159
160
  },
160
161
  required: ['summary'],
161
162
  },
@@ -386,6 +387,7 @@ function buildTicketCreateArgs(args) {
386
387
  cmdArgs.push(`--summary=${args.summary}`);
387
388
  if (args.description !== undefined) cmdArgs.push(`--description=${args.description}`);
388
389
  if (args.attachments?.length) cmdArgs.push(`--attach=${args.attachments.join(',')}`);
390
+ if (args.profile !== undefined) cmdArgs.push(`--profile=${args.profile}`);
389
391
  return cmdArgs;
390
392
  }
391
393
 
@@ -26,6 +26,11 @@ const REFERENCE_CONTEXT_WINDOW = 20;
26
26
  // rejoined before checking. See isLabelWord for what stops a run — and the
27
27
  // documented gap in inserting genuine dictionary words as separators.
28
28
  const MAX_JOINED_CHUNKS = 4;
29
+ // Real compound-word segments ("dual-store", "not-yet-configured") run short —
30
+ // capping segment length keeps a long random letters-only run from masquerading
31
+ // as one "segment" of a fake compound. See isHyphenatedWordCompound.
32
+ const MAX_COMPOUND_SEGMENT_LENGTH = 15;
33
+ const HYPHENATED_COMPOUND_RE = /^[A-Za-z]+(-[A-Za-z]+)+$/;
29
34
 
30
35
  const HARD_REJECT_PATTERNS = [
31
36
  { name: 'AWS access key', re: /AKIA[0-9A-Z]{16}/ },
@@ -124,29 +129,61 @@ function hasInternalCaseSwitch(token) {
124
129
  return /[a-z][A-Z]/.test(token);
125
130
  }
126
131
 
132
+ /**
133
+ * True for a letters-only, hyphen-delimited token whose segments are all
134
+ * short enough to read as a real compound word ("dual-store", "REDIS-vs-PG",
135
+ * "not-yet-configured") rather than a whitespace-split secret fragment. Only
136
+ * consulted from isLabelWord, i.e. only affects the stopAtLabelWords:true
137
+ * (generic-secret entropy) pass — joinedChunkRuns's stopAtLabelWords:false
138
+ * pass, which is what HARD_REJECT_PATTERNS relies on to catch a whitespace-
139
+ * split sk-/gsk_/AKIA/gh*_/eyJ/PEM-prefixed secret, never calls isLabelWord
140
+ * at all, so this cannot weaken that protection (see the regression test
141
+ * for exactly that shape, still passing after this change).
142
+ *
143
+ * The hasInternalCaseSwitch check matters the same way it does everywhere
144
+ * else in this file that distinguishes base64 content from prose (see
145
+ * isLabelWord's plain-word branch and looksLikeCodeFilename): without it, a
146
+ * base64-shaped fragment like "zqXvbNmKl-PoIuYtR" reads as a "compound word"
147
+ * purely because it happens to contain a hyphen, which would let it stop a
148
+ * run the same way a real compound does — the same category of shape-based
149
+ * exemption that made the code-filename bypass CRITICAL. Every real compound
150
+ * tested (dual-store, REDIS-vs-PG, well-known, not-yet-configured, PROD-DB,
151
+ * end-to-end, ...) has no internal case switch, so this costs nothing.
152
+ */
153
+ function isHyphenatedWordCompound(token) {
154
+ if (!HYPHENATED_COMPOUND_RE.test(token)) return false;
155
+ if (hasInternalCaseSwitch(token)) return false;
156
+ return token.split('-').every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
157
+ }
158
+
127
159
  /**
128
160
  * True for a token that stops a joined-chunk run: either a recognized git/
129
- * checksum label word ("commit", "sha256", "md5sum", ...), or an ordinary
130
- * English word (letters only, optionally with an internal possessive/
131
- * contraction apostrophe — "relay's", "doesn't" — but no base64-style case
132
- * switching). Anything else — a fragment containing a digit or other symbol,
133
- * a hyphenated compound, or an all-letter chunk that still reads as random
134
- * content — stays eligible to join, so a secret split by whitespace can still
135
- * be reassembled for the entropy check.
161
+ * checksum label word ("commit", "sha256", "md5sum", ...), a hyphenated
162
+ * compound word (see isHyphenatedWordCompound), or an ordinary English word
163
+ * (letters only, optionally with an internal possessive/contraction
164
+ * apostrophe — "relay's", "doesn't" — but no base64-style case switching).
165
+ * Anything else — a fragment containing a digit or other symbol, or an
166
+ * all-letter chunk that still reads as random content — stays eligible to
167
+ * join, so a secret split by whitespace can still be reassembled for the
168
+ * entropy check.
136
169
  *
137
170
  * The apostrophe allowance matters because without it, an ordinary possessive
138
171
  * next to another non-label token (e.g. a hyphenated compound: "relay's
139
172
  * decision-lookup") never gets a chance to stop the run — both fail to
140
173
  * qualify, so they concatenate into one artificial blob whose mixed
141
174
  * punctuation trips the entropy threshold. A real false positive, not a
142
- * hypothetical one (see the regression test below). Hyphenated tokens are
143
- * deliberately NOT given the same allowance: a short prefix + hyphen + long
144
- * letter-run is indistinguishable from a real compound word by shape alone
145
- * (e.g. "sk-abcdefghijklmnop", the letters-only half of a whitespace-split
146
- * API key) — see the adjacent regression test for exactly this shape. Once
147
- * the apostrophe fix lets "relay's" stop the run on its own, an adjacent
148
- * hyphenated compound is short enough on its own to fall under
149
- * MIN_RANDOM_TOKEN_LENGTH anyway, so it never needed the same allowance.
175
+ * hypothetical one (see the regression test below).
176
+ *
177
+ * Hyphenated compounds get the narrower isHyphenatedWordCompound allowance
178
+ * rather than the full apostrophe treatment: a short prefix + hyphen + one
179
+ * long unstructured letter-run (e.g. "sk-abcdefghijklmnop") still fails it —
180
+ * one segment exceeds MAX_COMPOUND_SEGMENT_LENGTH — so it stays eligible to
181
+ * join. This is belt-and-suspenders on top of the fact noted above that the
182
+ * hard-reject pass doesn't consult isLabelWord in the first place. Accepted
183
+ * trade-off: a hypothetical generic (non-hard-reject-shaped) secret that is
184
+ * itself letters-only, hyphen-delimited, and short-segmented would no longer
185
+ * bridge across a whitespace split via this path — no known real secret
186
+ * generator produces that shape (base32/64/hex alphabets are digit-inclusive).
150
187
  *
151
188
  * Known accepted gap: this can't tell a genuine possessive from one an
152
189
  * attacker deliberately chose as a separator to defeat the scanner (e.g.
@@ -161,6 +198,7 @@ function hasInternalCaseSwitch(token) {
161
198
  function isLabelWord(token) {
162
199
  const stripped = stripEdgePunctuation(token);
163
200
  if (GIT_REFERENCE_WORD_RE.test(stripped)) return true;
201
+ if (isHyphenatedWordCompound(stripped)) return true;
164
202
  return /^[A-Za-z]+(?:'[A-Za-z]+)*$/.test(stripped) && !hasInternalCaseSwitch(stripped);
165
203
  }
166
204