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
|
@@ -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 = '
|
|
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
|
-
|
|
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", ...),
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* be reassembled for the
|
|
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).
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
|
159
|
-
*
|
|
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
|
|