ticketlens 0.38.23 → 0.38.25

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.25",
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": {
@@ -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 = 'https://api.ticketlens.app';
6
+ export const DEFAULT_API_BASE = 'http://api.ticketlens.test';
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,
@@ -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,10 +26,24 @@ 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}/ },
32
- { name: 'private key block', re: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/ },
37
+ // \s* (not a literal space) between segments: this is the only entry with
38
+ // required internal spacing, and hardRejectRuns (no-separator rejoin) and
39
+ // despacedCombined (whitespace stripped) both destroy a literal space the
40
+ // same way they correctly neutralize whitespace-splitting on every other
41
+ // pattern here — so a tab, extra spaces, a newline, or no separator at all
42
+ // used to bypass this entry completely (backlog 1d). \s* closes all of
43
+ // those in one change since it's tested against combined, hardRejectRuns,
44
+ // and despacedCombined identically. JS's \s is already Unicode-aware
45
+ // (including U+FEFF), so no separate handling is needed here.
46
+ { name: 'private key block', re: /-----BEGIN\s*(RSA\s*|EC\s*|OPENSSH\s*|DSA\s*)?PRIVATE\s*KEY-----/ },
33
47
  { name: 'JSON Web Token (JWT)', re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
34
48
  { name: 'API key', re: /\b(sk-|gsk_)[A-Za-z0-9]{20,}\b/ },
35
49
  { name: 'GitHub token', re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/ },
@@ -124,29 +138,61 @@ function hasInternalCaseSwitch(token) {
124
138
  return /[a-z][A-Z]/.test(token);
125
139
  }
126
140
 
141
+ /**
142
+ * True for a letters-only, hyphen-delimited token whose segments are all
143
+ * short enough to read as a real compound word ("dual-store", "REDIS-vs-PG",
144
+ * "not-yet-configured") rather than a whitespace-split secret fragment. Only
145
+ * consulted from isLabelWord, i.e. only affects the stopAtLabelWords:true
146
+ * (generic-secret entropy) pass — joinedChunkRuns's stopAtLabelWords:false
147
+ * pass, which is what HARD_REJECT_PATTERNS relies on to catch a whitespace-
148
+ * split sk-/gsk_/AKIA/gh*_/eyJ/PEM-prefixed secret, never calls isLabelWord
149
+ * at all, so this cannot weaken that protection (see the regression test
150
+ * for exactly that shape, still passing after this change).
151
+ *
152
+ * The hasInternalCaseSwitch check matters the same way it does everywhere
153
+ * else in this file that distinguishes base64 content from prose (see
154
+ * isLabelWord's plain-word branch and looksLikeCodeFilename): without it, a
155
+ * base64-shaped fragment like "zqXvbNmKl-PoIuYtR" reads as a "compound word"
156
+ * purely because it happens to contain a hyphen, which would let it stop a
157
+ * run the same way a real compound does — the same category of shape-based
158
+ * exemption that made the code-filename bypass CRITICAL. Every real compound
159
+ * tested (dual-store, REDIS-vs-PG, well-known, not-yet-configured, PROD-DB,
160
+ * end-to-end, ...) has no internal case switch, so this costs nothing.
161
+ */
162
+ function isHyphenatedWordCompound(token) {
163
+ if (!HYPHENATED_COMPOUND_RE.test(token)) return false;
164
+ if (hasInternalCaseSwitch(token)) return false;
165
+ return token.split('-').every(segment => segment.length <= MAX_COMPOUND_SEGMENT_LENGTH);
166
+ }
167
+
127
168
  /**
128
169
  * 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.
170
+ * checksum label word ("commit", "sha256", "md5sum", ...), a hyphenated
171
+ * compound word (see isHyphenatedWordCompound), or an ordinary English word
172
+ * (letters only, optionally with an internal possessive/contraction
173
+ * apostrophe — "relay's", "doesn't" — but no base64-style case switching).
174
+ * Anything else — a fragment containing a digit or other symbol, or an
175
+ * all-letter chunk that still reads as random content — stays eligible to
176
+ * join, so a secret split by whitespace can still be reassembled for the
177
+ * entropy check.
136
178
  *
137
179
  * The apostrophe allowance matters because without it, an ordinary possessive
138
180
  * next to another non-label token (e.g. a hyphenated compound: "relay's
139
181
  * decision-lookup") never gets a chance to stop the run — both fail to
140
182
  * qualify, so they concatenate into one artificial blob whose mixed
141
183
  * 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.
184
+ * hypothetical one (see the regression test below).
185
+ *
186
+ * Hyphenated compounds get the narrower isHyphenatedWordCompound allowance
187
+ * rather than the full apostrophe treatment: a short prefix + hyphen + one
188
+ * long unstructured letter-run (e.g. "sk-abcdefghijklmnop") still fails it —
189
+ * one segment exceeds MAX_COMPOUND_SEGMENT_LENGTH — so it stays eligible to
190
+ * join. This is belt-and-suspenders on top of the fact noted above that the
191
+ * hard-reject pass doesn't consult isLabelWord in the first place. Accepted
192
+ * trade-off: a hypothetical generic (non-hard-reject-shaped) secret that is
193
+ * itself letters-only, hyphen-delimited, and short-segmented would no longer
194
+ * bridge across a whitespace split via this path — no known real secret
195
+ * generator produces that shape (base32/64/hex alphabets are digit-inclusive).
150
196
  *
151
197
  * Known accepted gap: this can't tell a genuine possessive from one an
152
198
  * attacker deliberately chose as a separator to defeat the scanner (e.g.
@@ -155,12 +201,18 @@ function hasInternalCaseSwitch(token) {
155
201
  * text into false-positive "random" strings (verified empirically — see the
156
202
  * regression test for "PROD-123456 for background"). Entropy-based detection
157
203
  * is a safety net, not a cryptographic guarantee; the well-known secret
158
- * shapes in HARD_REJECT_PATTERNS are the layer that's fully whitespace- and
159
- * word-insertion-proof, since they match a specific literal prefix.
204
+ * shapes in HARD_REJECT_PATTERNS are the layer that's whitespace-proof,
205
+ * since they match a specific literal prefix — with one exception: the
206
+ * private-key-block entry has two internal separator points (around the
207
+ * optional algorithm word and before KEY), and a word inserted at either
208
+ * one still defeats it, exactly as it did before backlog 1d's whitespace
209
+ * fix (that fix closed whitespace-substitution/removal, not word
210
+ * insertion — the same accepted-gap class as the possessive case above).
160
211
  */
161
212
  function isLabelWord(token) {
162
213
  const stripped = stripEdgePunctuation(token);
163
214
  if (GIT_REFERENCE_WORD_RE.test(stripped)) return true;
215
+ if (isHyphenatedWordCompound(stripped)) return true;
164
216
  return /^[A-Za-z]+(?:'[A-Za-z]+)*$/.test(stripped) && !hasInternalCaseSwitch(stripped);
165
217
  }
166
218