@taodigital/connectors 0.0.0-stage → 1.2.1
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/dist/azuredevops.d.ts +35 -0
- package/dist/azuredevops.js +116 -0
- package/dist/confluence.d.ts +22 -0
- package/dist/confluence.js +114 -0
- package/dist/connectors.test.d.ts +10 -0
- package/dist/connectors.test.js +407 -0
- package/dist/figma.d.ts +57 -0
- package/dist/figma.js +141 -0
- package/dist/github.d.ts +43 -0
- package/dist/github.js +120 -0
- package/dist/gitlab.d.ts +42 -0
- package/dist/gitlab.js +137 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +45 -0
- package/dist/jira.d.ts +93 -0
- package/dist/jira.js +354 -0
- package/dist/pagerduty.d.ts +25 -0
- package/dist/pagerduty.js +76 -0
- package/dist/salesforce.d.ts +35 -0
- package/dist/salesforce.js +93 -0
- package/dist/servicenow.d.ts +40 -0
- package/dist/servicenow.js +96 -0
- package/package.json +16 -4
- package/src/azuredevops.ts +141 -0
- package/src/confluence.ts +120 -0
- package/src/connectors.test.ts +424 -0
- package/src/figma.ts +194 -0
- package/src/github.ts +139 -0
- package/src/gitlab.ts +166 -0
- package/src/index.ts +9 -0
- package/src/jira.ts +372 -0
- package/src/pagerduty.ts +78 -0
- package/src/salesforce.ts +109 -0
- package/src/servicenow.ts +112 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -3
package/dist/jira.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* JIRA connector — credentials passed in, never read from env.
|
|
4
|
+
* baseUrl and email stored as-is; apiToken stored encrypted.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.testConnection = testConnection;
|
|
8
|
+
exports.listProjects = listProjects;
|
|
9
|
+
exports.listRecentIssues = listRecentIssues;
|
|
10
|
+
exports.getIssue = getIssue;
|
|
11
|
+
exports.createBug = createBug;
|
|
12
|
+
exports.listUserStories = listUserStories;
|
|
13
|
+
exports.createTestCase = createTestCase;
|
|
14
|
+
exports.searchIssues = searchIssues;
|
|
15
|
+
const core_1 = require("@taodigital/core");
|
|
16
|
+
function validateProxyUrl(proxyUrl) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(proxyUrl);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`Invalid proxy URL: ${proxyUrl}`);
|
|
23
|
+
}
|
|
24
|
+
const h = parsed.hostname.toLowerCase();
|
|
25
|
+
// Block loopback, link-local metadata
|
|
26
|
+
if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '169.254.169.254') {
|
|
27
|
+
throw new Error('Proxy URL must not target localhost or metadata addresses');
|
|
28
|
+
}
|
|
29
|
+
// Block RFC-1918 private ranges (10/8, 172.16-31/12, 192.168/16)
|
|
30
|
+
const parts = h.split('.').map(Number);
|
|
31
|
+
if (parts.length === 4 && !parts.some(isNaN)) {
|
|
32
|
+
const [a, b] = parts;
|
|
33
|
+
if (a === 10)
|
|
34
|
+
throw new Error('Proxy URL must not target private network (10.0.0.0/8)');
|
|
35
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
36
|
+
throw new Error('Proxy URL must not target private network (172.16.0.0/12)');
|
|
37
|
+
if (a === 192 && b === 168)
|
|
38
|
+
throw new Error('Proxy URL must not target private network (192.168.0.0/16)');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function makeFetch(creds) {
|
|
42
|
+
const TIMEOUT_MS = 15_000;
|
|
43
|
+
if (creds.proxyUrl) {
|
|
44
|
+
validateProxyUrl(creds.proxyUrl);
|
|
45
|
+
const { ProxyAgent } = await import('undici');
|
|
46
|
+
const dispatcher = new ProxyAgent(creds.proxyUrl);
|
|
47
|
+
return (url, init) => fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS), ...(init ?? {}), dispatcher });
|
|
48
|
+
}
|
|
49
|
+
// Wrap global fetch with a 15s timeout on every call
|
|
50
|
+
return (url, init) => fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS), ...(init ?? {}) });
|
|
51
|
+
}
|
|
52
|
+
function authHeader(creds) {
|
|
53
|
+
if (creds.bearerToken)
|
|
54
|
+
return `Bearer ${creds.bearerToken}`;
|
|
55
|
+
const secret = creds.password || creds.apiToken; // password = Server/DC, apiToken = Cloud
|
|
56
|
+
return 'Basic ' + Buffer.from(`${creds.email}:${secret}`).toString('base64');
|
|
57
|
+
}
|
|
58
|
+
async function testConnection(creds) {
|
|
59
|
+
try {
|
|
60
|
+
const doFetch = await makeFetch(creds);
|
|
61
|
+
if (!creds.proxyUrl)
|
|
62
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
63
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/myself`, {
|
|
64
|
+
headers: { Authorization: authHeader(creds), Accept: 'application/json' },
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok)
|
|
67
|
+
return { ok: false, error: `JIRA returned ${res.status}` };
|
|
68
|
+
const data = (await res.json());
|
|
69
|
+
return { ok: true, displayName: data.displayName };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return { ok: false, error: String(err) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function listProjects(creds) {
|
|
76
|
+
const doFetch = await makeFetch(creds);
|
|
77
|
+
if (!creds.proxyUrl)
|
|
78
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
79
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
80
|
+
const jiraCfg = await getSystemConfig('connectors_jira');
|
|
81
|
+
const pageSize = parseInt(String(jiraCfg?.pageSize ?? '50'), 10);
|
|
82
|
+
const MAX_PAGES = 10;
|
|
83
|
+
const allProjects = [];
|
|
84
|
+
let startAt = 0;
|
|
85
|
+
let truncated = false;
|
|
86
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
87
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/project/search?maxResults=${pageSize}&startAt=${startAt}`, {
|
|
88
|
+
headers: { Authorization: authHeader(creds), Accept: 'application/json' },
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
throw new Error(`JIRA error: ${res.status}`);
|
|
92
|
+
const data = (await res.json());
|
|
93
|
+
const values = data.values ?? [];
|
|
94
|
+
allProjects.push(...values);
|
|
95
|
+
const total = data.total ?? 0;
|
|
96
|
+
if (values.length < pageSize || allProjects.length >= total)
|
|
97
|
+
break;
|
|
98
|
+
startAt += values.length;
|
|
99
|
+
if (page === MAX_PAGES - 1) {
|
|
100
|
+
truncated = true;
|
|
101
|
+
console.warn(`listProjects: total=${total} exceeds page cap (${MAX_PAGES * pageSize}); results truncated`);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return allProjects.map((p) => ({ key: p.key, name: p.name, id: p.id }));
|
|
106
|
+
}
|
|
107
|
+
async function listRecentIssues(creds, maxResults) {
|
|
108
|
+
const doFetch = await makeFetch(creds);
|
|
109
|
+
if (!creds.proxyUrl)
|
|
110
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
111
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
112
|
+
const jiraCfg = await getSystemConfig('connectors_jira');
|
|
113
|
+
const resolvedMaxResults = maxResults ?? parseInt(String(jiraCfg?.recentPageSize ?? '20'), 10);
|
|
114
|
+
const jql = creds.projectKey
|
|
115
|
+
? `project = "${(creds.projectKey ?? '').replace(/"/g, '\\"')}" ORDER BY updated DESC`
|
|
116
|
+
: 'ORDER BY updated DESC';
|
|
117
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=${resolvedMaxResults}&fields=summary,status,priority,assignee,updated`, { headers: { Authorization: authHeader(creds), Accept: 'application/json' } });
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
throw new Error(`JIRA error: ${res.status}`);
|
|
120
|
+
const data = (await res.json());
|
|
121
|
+
return (data.issues ?? []).map((i) => ({
|
|
122
|
+
key: i.key,
|
|
123
|
+
summary: i.fields?.summary,
|
|
124
|
+
status: i.fields?.status?.name,
|
|
125
|
+
priority: i.fields?.priority?.name,
|
|
126
|
+
assignee: i.fields?.assignee?.displayName,
|
|
127
|
+
updated: i.fields?.updated,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
async function getIssue(creds, issueKey, options = {}) {
|
|
131
|
+
const doFetch = await makeFetch(creds);
|
|
132
|
+
if (!creds.proxyUrl)
|
|
133
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
134
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
135
|
+
const jiraCfg = await getSystemConfig('connectors_jira');
|
|
136
|
+
const acField = options.acceptanceCriteriaFieldId ?? String(jiraCfg?.acFieldId ?? '');
|
|
137
|
+
const fieldsParam = `summary,description,status,priority,issuetype,labels,components,issuelinks,parent,subtasks${acField ? `,${acField}` : ''}`;
|
|
138
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/issue/${issueKey}?fields=${encodeURIComponent(fieldsParam)}`, {
|
|
139
|
+
headers: { Authorization: authHeader(creds), Accept: 'application/json' },
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok)
|
|
142
|
+
throw new Error(`JIRA error: ${res.status}`);
|
|
143
|
+
const i = (await res.json());
|
|
144
|
+
return {
|
|
145
|
+
key: i.key,
|
|
146
|
+
summary: i.fields?.summary,
|
|
147
|
+
description: i.fields?.description,
|
|
148
|
+
status: i.fields?.status?.name,
|
|
149
|
+
priority: i.fields?.priority?.name,
|
|
150
|
+
issueType: i.fields?.issuetype?.name,
|
|
151
|
+
acceptanceCriteria: acField ? (i.fields?.[acField] ?? null) : null,
|
|
152
|
+
labels: i.fields?.labels ?? [],
|
|
153
|
+
components: (i.fields?.components ?? []).map((c) => c.name),
|
|
154
|
+
epic: i.fields?.parent ? { key: i.fields.parent.key, summary: i.fields.parent.fields?.summary } : null,
|
|
155
|
+
links: (i.fields?.issuelinks ?? []).map((l) => ({
|
|
156
|
+
type: l.type?.name ?? l.type?.inward ?? l.type?.outward ?? 'relates',
|
|
157
|
+
key: l.inwardIssue?.key ?? l.outwardIssue?.key,
|
|
158
|
+
summary: l.inwardIssue?.fields?.summary ?? l.outwardIssue?.fields?.summary,
|
|
159
|
+
})).filter((x) => Boolean(x.key)),
|
|
160
|
+
subtasks: (i.fields?.subtasks ?? []).map((st) => ({ key: st.key, summary: st.fields?.summary })),
|
|
161
|
+
url: `${creds.baseUrl}/browse/${i.key}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function createBug(creds, opts) {
|
|
165
|
+
const doFetch = await makeFetch(creds);
|
|
166
|
+
if (!creds.proxyUrl)
|
|
167
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
168
|
+
if (!creds.projectKey)
|
|
169
|
+
throw new Error('projectKey required to create JIRA issues.');
|
|
170
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/issue`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: authHeader(creds),
|
|
174
|
+
Accept: 'application/json',
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
fields: {
|
|
179
|
+
project: { key: creds.projectKey },
|
|
180
|
+
summary: opts.summary,
|
|
181
|
+
description: {
|
|
182
|
+
type: 'doc',
|
|
183
|
+
version: 1,
|
|
184
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: opts.description }] }],
|
|
185
|
+
},
|
|
186
|
+
issuetype: { name: 'Bug' },
|
|
187
|
+
priority: { name: opts.priority ?? opts.defaultBugPriority ?? 'High' },
|
|
188
|
+
labels: opts.labels ?? (opts.defaultBugLabel ? [opts.defaultBugLabel] : []),
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok)
|
|
193
|
+
throw new Error(`JIRA error: ${res.status} ${await res.text()}`);
|
|
194
|
+
const created = (await res.json());
|
|
195
|
+
return { key: created.key, url: `${creds.baseUrl}/browse/${created.key}` };
|
|
196
|
+
}
|
|
197
|
+
async function listUserStories(creds, sprintId, maxResults, options = {}) {
|
|
198
|
+
const doFetch = await makeFetch(creds);
|
|
199
|
+
if (!creds.proxyUrl)
|
|
200
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
201
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
202
|
+
const jiraCfg = await getSystemConfig('connectors_jira');
|
|
203
|
+
const pageSize = Math.min(maxResults ?? parseInt(String(jiraCfg?.pageSize ?? '50'), 10), 100);
|
|
204
|
+
const acField = options.acceptanceCriteriaFieldId ?? String(jiraCfg?.acFieldId ?? '');
|
|
205
|
+
const jql = sprintId
|
|
206
|
+
? `project = "${(creds.projectKey ?? '').replace(/"/g, '\\"')}" AND sprint = ${sprintId} AND issuetype = Story ORDER BY priority DESC`
|
|
207
|
+
: `project = "${(creds.projectKey ?? '').replace(/"/g, '\\"')}" AND issuetype = Story ORDER BY updated DESC`;
|
|
208
|
+
const fieldsParam = `summary,description,status,priority,issuetype,labels,components,issuelinks,parent,subtasks${acField ? `,${acField}` : ''}`;
|
|
209
|
+
const MAX_PAGES = 20;
|
|
210
|
+
const allIssues = [];
|
|
211
|
+
let startAt = 0;
|
|
212
|
+
let truncated = false;
|
|
213
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
214
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=${pageSize}&startAt=${startAt}&fields=${encodeURIComponent(fieldsParam)}`, { headers: { Authorization: authHeader(creds), Accept: 'application/json' } });
|
|
215
|
+
if (!res.ok)
|
|
216
|
+
throw new Error(`JIRA error: ${res.status}`);
|
|
217
|
+
const data = (await res.json());
|
|
218
|
+
const issues = data.issues ?? [];
|
|
219
|
+
allIssues.push(...issues);
|
|
220
|
+
const total = data.total ?? 0;
|
|
221
|
+
if (issues.length < pageSize || allIssues.length >= total)
|
|
222
|
+
break;
|
|
223
|
+
startAt += issues.length;
|
|
224
|
+
if (page === MAX_PAGES - 1) {
|
|
225
|
+
truncated = true;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const mapped = allIssues.map((i) => ({
|
|
230
|
+
key: i.key,
|
|
231
|
+
summary: i.fields?.summary,
|
|
232
|
+
description: i.fields?.description,
|
|
233
|
+
acceptanceCriteria: acField ? (i.fields?.[acField] ?? null) : null,
|
|
234
|
+
status: i.fields?.status?.name,
|
|
235
|
+
priority: i.fields?.priority?.name,
|
|
236
|
+
issueType: i.fields?.issuetype?.name,
|
|
237
|
+
labels: i.fields?.labels ?? [],
|
|
238
|
+
components: (i.fields?.components ?? []).map((c) => c.name),
|
|
239
|
+
epic: i.fields?.parent
|
|
240
|
+
? { key: i.fields.parent.key, summary: i.fields.parent.summary ?? i.fields.parent.fields?.summary ?? null }
|
|
241
|
+
: null,
|
|
242
|
+
links: (i.fields?.issuelinks ?? []).map((l) => ({
|
|
243
|
+
type: l.type?.name ?? l.type?.inward ?? l.type?.outward ?? 'relates',
|
|
244
|
+
key: l.inwardIssue?.key ?? l.outwardIssue?.key,
|
|
245
|
+
summary: l.inwardIssue?.fields?.summary ?? l.outwardIssue?.fields?.summary,
|
|
246
|
+
})).filter((x) => Boolean(x.key)),
|
|
247
|
+
subtasks: (i.fields?.subtasks ?? []).map((st) => ({ key: st.key, summary: st.fields?.summary })),
|
|
248
|
+
url: `${creds.baseUrl}/browse/${i.key}`,
|
|
249
|
+
}));
|
|
250
|
+
return { stories: mapped, truncated };
|
|
251
|
+
}
|
|
252
|
+
async function createTestCase(creds, opts) {
|
|
253
|
+
const doFetch = await makeFetch(creds);
|
|
254
|
+
if (!creds.proxyUrl)
|
|
255
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
256
|
+
if (!creds.projectKey)
|
|
257
|
+
throw new Error('projectKey required to create Jira test cases.');
|
|
258
|
+
const { stepsHtml } = opts;
|
|
259
|
+
const stepLines = (stepsHtml ?? '')
|
|
260
|
+
.split(/<br\s*\/?>|<\/li>/i)
|
|
261
|
+
.map((s) => s.replace(/<[^>]+>/g, '').trim())
|
|
262
|
+
.filter(Boolean);
|
|
263
|
+
const stepsNode = stepLines.length > 0
|
|
264
|
+
? {
|
|
265
|
+
type: 'orderedList',
|
|
266
|
+
content: stepLines.map((t) => ({
|
|
267
|
+
type: 'listItem',
|
|
268
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: t }] }]
|
|
269
|
+
}))
|
|
270
|
+
}
|
|
271
|
+
: { type: 'paragraph', content: [{ type: 'text', text: stepsHtml ?? '' }] };
|
|
272
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/issue`, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: {
|
|
275
|
+
Authorization: authHeader(creds),
|
|
276
|
+
Accept: 'application/json',
|
|
277
|
+
'Content-Type': 'application/json',
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
fields: {
|
|
281
|
+
project: { key: creds.projectKey },
|
|
282
|
+
summary: opts.summary,
|
|
283
|
+
description: {
|
|
284
|
+
type: 'doc',
|
|
285
|
+
version: 1,
|
|
286
|
+
content: [stepsNode],
|
|
287
|
+
},
|
|
288
|
+
// 'Test' issuetype requires Xray; falls back to 'Story' if not found (Jira returns 400)
|
|
289
|
+
issuetype: { name: opts.defaultIssueType ?? 'Test' },
|
|
290
|
+
priority: { name: opts.priority ?? 'High' },
|
|
291
|
+
labels: (opts.labels ?? []).map((l) => l.replace(/^@/, '')),
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
if (!res.ok) {
|
|
296
|
+
const errText = await res.text().catch(() => '');
|
|
297
|
+
// Xray 'Test' issue type not available — retry as Story
|
|
298
|
+
if (res.status === 400 && errText.includes('issuetype')) {
|
|
299
|
+
const retry = await doFetch(`${creds.baseUrl}/rest/api/3/issue`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: {
|
|
302
|
+
Authorization: authHeader(creds),
|
|
303
|
+
Accept: 'application/json',
|
|
304
|
+
'Content-Type': 'application/json',
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
fields: {
|
|
308
|
+
project: { key: creds.projectKey },
|
|
309
|
+
summary: opts.summary,
|
|
310
|
+
description: {
|
|
311
|
+
type: 'doc',
|
|
312
|
+
version: 1,
|
|
313
|
+
content: [stepsNode],
|
|
314
|
+
},
|
|
315
|
+
issuetype: { name: 'Story' },
|
|
316
|
+
priority: { name: opts.priority ?? 'High' },
|
|
317
|
+
labels: [...(opts.defaultTestCaseLabel ? [opts.defaultTestCaseLabel] : []), ...(opts.labels ?? []).map((l) => l.replace(/^@/, ''))],
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
if (!retry.ok)
|
|
322
|
+
throw new Error(`Jira error: ${retry.status} ${await retry.text().catch(() => '')}`);
|
|
323
|
+
const d = (await retry.json());
|
|
324
|
+
return { key: d.key, url: `${creds.baseUrl}/browse/${d.key}` };
|
|
325
|
+
}
|
|
326
|
+
throw new Error(`Jira error: ${res.status} ${errText.slice(0, 200)}`);
|
|
327
|
+
}
|
|
328
|
+
const created = (await res.json());
|
|
329
|
+
return { key: created.key, url: `${creds.baseUrl}/browse/${created.key}` };
|
|
330
|
+
}
|
|
331
|
+
/** Free-text search — finds issues that mention a screen URL or name. */
|
|
332
|
+
async function searchIssues(creds, text, maxResults) {
|
|
333
|
+
const doFetch = await makeFetch(creds);
|
|
334
|
+
if (!creds.proxyUrl)
|
|
335
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
|
|
336
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
337
|
+
const jiraCfg = await getSystemConfig('connectors_jira');
|
|
338
|
+
const resolvedMaxResults = maxResults ?? parseInt(String(jiraCfg?.searchPageSize ?? '10'), 10);
|
|
339
|
+
const safeText = text.replace(/["\\]/g, ' ').trim();
|
|
340
|
+
const projectClause = creds.projectKey ? `project = "${(creds.projectKey ?? '').replace(/"/g, '\\"')}" AND ` : '';
|
|
341
|
+
const jql = `${projectClause}text ~ "${safeText}" ORDER BY updated DESC`;
|
|
342
|
+
const res = await doFetch(`${creds.baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=${resolvedMaxResults}&fields=summary,status,priority,updated`, { headers: { Authorization: authHeader(creds), Accept: 'application/json' } });
|
|
343
|
+
if (!res.ok)
|
|
344
|
+
throw new Error(`Jira search failed: ${res.status} ${await res.text().catch(() => '')}`);
|
|
345
|
+
const data = (await res.json());
|
|
346
|
+
return (data.issues ?? []).map((i) => ({
|
|
347
|
+
key: i.key,
|
|
348
|
+
summary: i.fields?.summary,
|
|
349
|
+
status: i.fields?.status?.name,
|
|
350
|
+
priority: i.fields?.priority?.name,
|
|
351
|
+
updated: i.fields?.updated,
|
|
352
|
+
url: `${creds.baseUrl}/browse/${i.key}`,
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PagerDuty connector — alert on critical test failures.
|
|
3
|
+
* API key stored encrypted per project.
|
|
4
|
+
*/
|
|
5
|
+
export interface PagerDutyCredentials {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
serviceId?: string;
|
|
8
|
+
escalationPolicyId?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function testConnection(creds: PagerDutyCredentials): Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function createIncident(creds: PagerDutyCredentials, opts: {
|
|
15
|
+
title: string;
|
|
16
|
+
body: string;
|
|
17
|
+
severity: 'critical' | 'error' | 'warning' | 'info';
|
|
18
|
+
severityMap?: Partial<Record<'critical' | 'error' | 'warning' | 'info', 'high' | 'low'>>;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
id: any;
|
|
21
|
+
url: any;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function resolveIncident(creds: PagerDutyCredentials, incidentId: string): Promise<{
|
|
24
|
+
resolved: boolean;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PagerDuty connector — alert on critical test failures.
|
|
4
|
+
* API key stored encrypted per project.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.testConnection = testConnection;
|
|
8
|
+
exports.createIncident = createIncident;
|
|
9
|
+
exports.resolveIncident = resolveIncident;
|
|
10
|
+
function headers(creds, fromEmail = '') {
|
|
11
|
+
return {
|
|
12
|
+
Authorization: `Token token=${creds.apiKey}`,
|
|
13
|
+
Accept: 'application/vnd.pagerduty+json;version=2',
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
From: fromEmail,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function testConnection(creds) {
|
|
19
|
+
try {
|
|
20
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
21
|
+
const pdCfg = await getSystemConfig('connectors_pagerduty');
|
|
22
|
+
const fromEmail = String(pdCfg?.fromEmail ?? '');
|
|
23
|
+
const res = await fetch('https://api.pagerduty.com/users/me', { headers: headers(creds, fromEmail) });
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
return { ok: false, error: `PagerDuty returned ${res.status}` };
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return { ok: false, error: String(err) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function createIncident(creds, opts) {
|
|
33
|
+
if (!creds.serviceId)
|
|
34
|
+
throw new Error('PagerDuty serviceId is required to create incidents.');
|
|
35
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
36
|
+
const pdCfg = await getSystemConfig('connectors_pagerduty');
|
|
37
|
+
const fromEmail = String(pdCfg?.fromEmail ?? '');
|
|
38
|
+
const defaultSeverityMap = (() => {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(String(pdCfg?.severityMap ?? '{"critical":"high","error":"high","warning":"medium","info":"low"}'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { critical: 'high', error: 'high' };
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
const res = await fetch('https://api.pagerduty.com/incidents', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: headers(creds, fromEmail),
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
incident: {
|
|
51
|
+
type: 'incident',
|
|
52
|
+
title: opts.title,
|
|
53
|
+
service: { id: creds.serviceId, type: 'service_reference' },
|
|
54
|
+
urgency: (opts.severityMap ?? defaultSeverityMap)[opts.severity] ?? 'low',
|
|
55
|
+
body: { type: 'incident_body', details: opts.body },
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok)
|
|
60
|
+
throw new Error(`PagerDuty error: ${res.status} ${await res.text()}`);
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
return { id: data.incident?.id, url: data.incident?.html_url };
|
|
63
|
+
}
|
|
64
|
+
async function resolveIncident(creds, incidentId) {
|
|
65
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
66
|
+
const pdCfg = await getSystemConfig('connectors_pagerduty');
|
|
67
|
+
const fromEmail = String(pdCfg?.fromEmail ?? '');
|
|
68
|
+
const res = await fetch(`https://api.pagerduty.com/incidents/${incidentId}`, {
|
|
69
|
+
method: 'PUT',
|
|
70
|
+
headers: headers(creds, fromEmail),
|
|
71
|
+
body: JSON.stringify({ incident: { type: 'incident', status: 'resolved' } }),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok)
|
|
74
|
+
throw new Error(`PagerDuty error: ${res.status}`);
|
|
75
|
+
return { resolved: true };
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Salesforce connector — OAuth2 client credentials flow.
|
|
3
|
+
* clientId + encrypted clientSecret stored per project.
|
|
4
|
+
*/
|
|
5
|
+
export interface SalesforceCredentials {
|
|
6
|
+
instanceUrl: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function testConnection(creds: SalesforceCredentials): Promise<{
|
|
13
|
+
ok: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function listObjects(creds: SalesforceCredentials): Promise<any>;
|
|
17
|
+
export declare function queryRecords(creds: SalesforceCredentials, soql: string): Promise<{
|
|
18
|
+
totalSize: any;
|
|
19
|
+
records: any;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function createCase(creds: SalesforceCredentials, opts: {
|
|
22
|
+
subject: string;
|
|
23
|
+
description: string;
|
|
24
|
+
priority?: 'High' | 'Medium' | 'Low';
|
|
25
|
+
origin?: string;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
id: any;
|
|
28
|
+
url: string;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function listCases(creds: SalesforceCredentials, opts?: {
|
|
31
|
+
limit?: number;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
totalSize: any;
|
|
34
|
+
records: any;
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Salesforce connector — OAuth2 client credentials flow.
|
|
4
|
+
* clientId + encrypted clientSecret stored per project.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.testConnection = testConnection;
|
|
8
|
+
exports.listObjects = listObjects;
|
|
9
|
+
exports.queryRecords = queryRecords;
|
|
10
|
+
exports.createCase = createCase;
|
|
11
|
+
exports.listCases = listCases;
|
|
12
|
+
const core_1 = require("@taodigital/core");
|
|
13
|
+
async function getAccessToken(creds) {
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
grant_type: 'client_credentials',
|
|
16
|
+
client_id: creds.clientId,
|
|
17
|
+
client_secret: creds.clientSecret,
|
|
18
|
+
});
|
|
19
|
+
const res = await fetch(`${creds.instanceUrl}/services/oauth2/token`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
22
|
+
body: params.toString(),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`Salesforce OAuth error: ${res.status} ${await res.text()}`);
|
|
26
|
+
const data = (await res.json());
|
|
27
|
+
return data.access_token;
|
|
28
|
+
}
|
|
29
|
+
async function testConnection(creds) {
|
|
30
|
+
try {
|
|
31
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
32
|
+
await getAccessToken(creds);
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return { ok: false, error: String(err) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function listObjects(creds) {
|
|
40
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
41
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
42
|
+
const sfCfg = await getSystemConfig('connectors_salesforce');
|
|
43
|
+
const apiVersion = String(sfCfg?.apiVersion ?? 'v59.0');
|
|
44
|
+
const token = await getAccessToken(creds);
|
|
45
|
+
const res = await fetch(`${creds.instanceUrl}/services/data/${apiVersion}/sobjects/`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(`Salesforce error: ${res.status}`);
|
|
50
|
+
const data = (await res.json());
|
|
51
|
+
return (data.sobjects ?? []).map((o) => ({ name: o.name, label: o.label, custom: o.custom }));
|
|
52
|
+
}
|
|
53
|
+
async function queryRecords(creds, soql) {
|
|
54
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
55
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
56
|
+
const sfCfg = await getSystemConfig('connectors_salesforce');
|
|
57
|
+
const apiVersion = String(sfCfg?.apiVersion ?? 'v59.0');
|
|
58
|
+
const token = await getAccessToken(creds);
|
|
59
|
+
const res = await fetch(`${creds.instanceUrl}/services/data/${apiVersion}/query?q=${encodeURIComponent(soql)}`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } });
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
throw new Error(`Salesforce error: ${res.status}`);
|
|
62
|
+
const data = (await res.json());
|
|
63
|
+
return { totalSize: data.totalSize, records: data.records ?? [] };
|
|
64
|
+
}
|
|
65
|
+
async function createCase(creds, opts) {
|
|
66
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
67
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
68
|
+
const sfCfg = await getSystemConfig('connectors_salesforce');
|
|
69
|
+
const apiVersion = String(sfCfg?.apiVersion ?? 'v59.0');
|
|
70
|
+
const token = await getAccessToken(creds);
|
|
71
|
+
const res = await fetch(`${creds.instanceUrl}/services/data/${apiVersion}/sobjects/Case/`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${token}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
Accept: 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
Subject: opts.subject,
|
|
80
|
+
Description: opts.description,
|
|
81
|
+
Priority: opts.priority ?? 'High',
|
|
82
|
+
Origin: opts.origin ?? String(sfCfg?.caseOrigin ?? ''),
|
|
83
|
+
Status: 'New',
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
throw new Error(`Salesforce error: ${res.status} ${await res.text()}`);
|
|
88
|
+
const data = (await res.json());
|
|
89
|
+
return { id: data.id, url: `${creds.instanceUrl}/lightning/r/Case/${data.id}/view` };
|
|
90
|
+
}
|
|
91
|
+
async function listCases(creds, opts) {
|
|
92
|
+
return queryRecords(creds, `SELECT Id, CaseNumber, Subject, Status, Priority, CreatedDate, Description FROM Case ORDER BY CreatedDate DESC LIMIT ${opts?.limit ?? 25}`);
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceNow connector — incident and change management integration.
|
|
3
|
+
* Credentials stored encrypted per project.
|
|
4
|
+
*/
|
|
5
|
+
export interface ServiceNowCredentials {
|
|
6
|
+
instanceUrl: string;
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function testConnection(creds: ServiceNowCredentials): Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function createIncident(creds: ServiceNowCredentials, opts: {
|
|
15
|
+
shortDescription: string;
|
|
16
|
+
description: string;
|
|
17
|
+
urgency?: 1 | 2 | 3;
|
|
18
|
+
impact?: 1 | 2 | 3;
|
|
19
|
+
category?: string;
|
|
20
|
+
defaultUrgency?: 1 | 2 | 3;
|
|
21
|
+
defaultImpact?: 1 | 2 | 3;
|
|
22
|
+
defaultCategory?: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
sysId: any;
|
|
25
|
+
number: any;
|
|
26
|
+
url: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function createChangeRequest(creds: ServiceNowCredentials, opts: {
|
|
29
|
+
shortDescription: string;
|
|
30
|
+
description: string;
|
|
31
|
+
type?: 'normal' | 'standard' | 'emergency';
|
|
32
|
+
defaultType?: 'normal' | 'standard' | 'emergency';
|
|
33
|
+
}): Promise<{
|
|
34
|
+
sysId: any;
|
|
35
|
+
number: any;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function listIncidents(creds: ServiceNowCredentials, opts?: {
|
|
38
|
+
limit?: number;
|
|
39
|
+
state?: string;
|
|
40
|
+
}): Promise<any[]>;
|