ticketlens 0.9.8 → 0.9.9
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
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.9.
|
|
1
|
+
<!-- jtb-skill-version: 0.9.9 -->
|
|
2
2
|
---
|
|
3
3
|
name: jtb
|
|
4
4
|
description: Fetch a Jira ticket's full context (description, comments, linked issues, code references) and assemble a structured TicketBrief for implementation planning. Use when user types /jtb, mentions a Jira ticket key, or wants to plan work from a Jira ticket.
|
|
@@ -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 = 'https://api.ticketlens.app';
|
|
7
7
|
|
|
8
8
|
// Matches localhost, 127.0.0.1, and any hostname ending in .test or .local,
|
|
9
9
|
// with an optional port — all treated as local-only addresses.
|
|
@@ -83,7 +83,8 @@ export async function downloadAttachments(ticket, opts = {}) {
|
|
|
83
83
|
onProgress?.(` download ${a.filename}`);
|
|
84
84
|
try {
|
|
85
85
|
const headers = buildAuthHeader(env);
|
|
86
|
-
|
|
86
|
+
// redirect:'error' prevents a Jira redirect from leaking auth headers to a different origin.
|
|
87
|
+
const response = await fetcher(a.content, { headers, redirect: 'error' });
|
|
87
88
|
if (!response.ok) throw new Error(`HTTP ${response.status} (${response.statusText})`);
|
|
88
89
|
const buffer = await response.arrayBuffer();
|
|
89
90
|
fs.writeFileSync(localPath, Buffer.from(buffer));
|
|
@@ -19,7 +19,14 @@ export const DEFAULT_BRIEF_TTL = '4h'; // human-readable default for display/con
|
|
|
19
19
|
export function briefCachePath(ticketKey, profileName, configDir = DEFAULT_CONFIG_DIR) {
|
|
20
20
|
const safeProfile = (profileName || '_default').replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
21
21
|
const safeKey = ticketKey.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
22
|
-
|
|
22
|
+
const resolvedDir = path.resolve(configDir);
|
|
23
|
+
const result = path.join(resolvedDir, 'cache', safeProfile, safeKey, 'brief.json');
|
|
24
|
+
// Defense-in-depth: ensure the final path cannot escape the config directory,
|
|
25
|
+
// even if configDir itself is manipulated or the sanitization above is weakened.
|
|
26
|
+
if (!result.startsWith(resolvedDir + path.sep)) {
|
|
27
|
+
throw new Error(`Cache path escapes config directory: ${result}`);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
@@ -81,6 +81,35 @@ export function normalizeTicket(raw) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// RFC-1918 + link-local + loopback + localhost blocked to prevent SSRF.
|
|
85
|
+
const BLOCKED_PATTERNS = [
|
|
86
|
+
/^127\./,
|
|
87
|
+
/^10\./,
|
|
88
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
89
|
+
/^192\.168\./,
|
|
90
|
+
/^169\.254\./,
|
|
91
|
+
/^0\.0\.0\.0$/,
|
|
92
|
+
/^::1$/,
|
|
93
|
+
/^::ffff:/i,
|
|
94
|
+
/^fc00:/i,
|
|
95
|
+
/^fe80:/i,
|
|
96
|
+
/^localhost$/i,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export function validateBaseUrl(url) {
|
|
100
|
+
let parsed;
|
|
101
|
+
try { parsed = new URL(url); } catch {
|
|
102
|
+
throw new Error(`JIRA_BASE_URL is not a valid URL: ${url}`);
|
|
103
|
+
}
|
|
104
|
+
if (parsed.protocol !== 'https:') {
|
|
105
|
+
throw new Error(`JIRA_BASE_URL must use HTTPS (got ${parsed.protocol})`);
|
|
106
|
+
}
|
|
107
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
|
|
108
|
+
if (BLOCKED_PATTERNS.some(re => re.test(hostname))) {
|
|
109
|
+
throw new Error(`JIRA_BASE_URL hostname is blocked (${hostname})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
export function buildAuthHeader(env) {
|
|
85
114
|
if (env.JIRA_PAT) {
|
|
86
115
|
return { Authorization: `Bearer ${env.JIRA_PAT}` };
|
|
@@ -91,6 +120,7 @@ export function buildAuthHeader(env) {
|
|
|
91
120
|
|
|
92
121
|
export async function fetchCurrentUser(opts = {}) {
|
|
93
122
|
const { env = process.env, fetcher = globalThis.fetch, apiVersion = 2, timeoutMs = 10_000 } = opts;
|
|
123
|
+
validateBaseUrl(env.JIRA_BASE_URL);
|
|
94
124
|
const baseUrl = env.JIRA_BASE_URL.replace(/\/$/, '');
|
|
95
125
|
const headers = { ...buildAuthHeader(env), 'Content-Type': 'application/json' };
|
|
96
126
|
|
|
@@ -116,6 +146,7 @@ export async function fetchCurrentUser(opts = {}) {
|
|
|
116
146
|
|
|
117
147
|
export async function fetchStatuses(opts = {}) {
|
|
118
148
|
const { env = process.env, fetcher = globalThis.fetch, apiVersion = 2, timeoutMs = 10_000 } = opts;
|
|
149
|
+
validateBaseUrl(env.JIRA_BASE_URL);
|
|
119
150
|
const baseUrl = env.JIRA_BASE_URL.replace(/\/$/, '');
|
|
120
151
|
const headers = { ...buildAuthHeader(env), 'Content-Type': 'application/json' };
|
|
121
152
|
|
|
@@ -136,6 +167,7 @@ export async function fetchStatuses(opts = {}) {
|
|
|
136
167
|
|
|
137
168
|
export async function searchTickets(jql, opts = {}) {
|
|
138
169
|
const { env = process.env, fetcher = globalThis.fetch, maxResults = 50, apiVersion = 2, timeoutMs = 10_000, expandChangelog = false } = opts;
|
|
170
|
+
validateBaseUrl(env.JIRA_BASE_URL);
|
|
139
171
|
const baseUrl = env.JIRA_BASE_URL.replace(/\/$/, '');
|
|
140
172
|
const headers = { ...buildAuthHeader(env), 'Content-Type': 'application/json' };
|
|
141
173
|
|
|
@@ -163,6 +195,7 @@ export async function searchTickets(jql, opts = {}) {
|
|
|
163
195
|
|
|
164
196
|
export async function fetchRemoteLinks(ticketKey, opts = {}) {
|
|
165
197
|
const { env = process.env, fetcher = globalThis.fetch, apiVersion = 2, timeoutMs = 10_000 } = opts;
|
|
198
|
+
validateBaseUrl(env.JIRA_BASE_URL);
|
|
166
199
|
const baseUrl = env.JIRA_BASE_URL.replace(/\/$/, '');
|
|
167
200
|
const url = `${baseUrl}/rest/api/${apiVersion}/issue/${encodeURIComponent(ticketKey)}/remotelink`;
|
|
168
201
|
|
|
@@ -180,6 +213,7 @@ export async function fetchRemoteLinks(ticketKey, opts = {}) {
|
|
|
180
213
|
|
|
181
214
|
export async function fetchTicket(ticketKey, opts = {}) {
|
|
182
215
|
const { env = process.env, fetcher = globalThis.fetch, depth = 1, apiVersion = 2, timeoutMs = 10_000, expandChangelog = false, _visited = new Set(), _currentDepth = 0 } = opts;
|
|
216
|
+
validateBaseUrl(env.JIRA_BASE_URL);
|
|
183
217
|
const baseUrl = env.JIRA_BASE_URL.replace(/\/$/, '');
|
|
184
218
|
const headers = { ...buildAuthHeader(env), 'Content-Type': 'application/json' };
|
|
185
219
|
|