@taodigital/connectors 0.0.0-stage → 1.2.0

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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Azure DevOps connector — credentials passed in, never read from env.
3
+ * Uses a Personal Access Token (PAT) stored encrypted in Connector.config.
4
+ *
5
+ * API reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/
6
+ */
7
+ export interface AzureDevOpsCredentials {
8
+ organization: string;
9
+ project: string;
10
+ pat: string;
11
+ areaPath?: string;
12
+ }
13
+ export declare function testConnection(creds: AzureDevOpsCredentials): Promise<{
14
+ ok: boolean;
15
+ login?: string;
16
+ error?: string;
17
+ }>;
18
+ export declare function listRecentWorkItems(creds: AzureDevOpsCredentials, types?: string[], limit?: number): Promise<Array<{
19
+ id: number;
20
+ title: string;
21
+ type: string;
22
+ state: string;
23
+ url: string;
24
+ }>>;
25
+ export declare function createBug(creds: AzureDevOpsCredentials, title: string, description: string, priority?: 1 | 2 | 3 | 4): Promise<{
26
+ id: number;
27
+ url: string;
28
+ title: string;
29
+ }>;
30
+ export declare function listPipelines(creds: AzureDevOpsCredentials, limit?: number): Promise<Array<{
31
+ id: number;
32
+ name: string;
33
+ status: string;
34
+ url: string;
35
+ }>>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * Azure DevOps connector — credentials passed in, never read from env.
4
+ * Uses a Personal Access Token (PAT) stored encrypted in Connector.config.
5
+ *
6
+ * API reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.testConnection = testConnection;
10
+ exports.listRecentWorkItems = listRecentWorkItems;
11
+ exports.createBug = createBug;
12
+ exports.listPipelines = listPipelines;
13
+ const core_1 = require("@taodigital/core");
14
+ const API_VERSION = '7.1';
15
+ function apiBase(creds) {
16
+ return `https://dev.azure.com/${encodeURIComponent(creds.organization)}/${encodeURIComponent(creds.project)}/_apis`;
17
+ }
18
+ function authHeaders(pat) {
19
+ const encoded = Buffer.from(`:${pat}`).toString('base64');
20
+ return {
21
+ Authorization: `Basic ${encoded}`,
22
+ 'Content-Type': 'application/json',
23
+ };
24
+ }
25
+ async function testConnection(creds) {
26
+ try {
27
+ await (0, core_1.assertResolvesToPublicAddress)('https://dev.azure.com');
28
+ const base = apiBase(creds);
29
+ const res = await fetch(`${base}/wit/wiql?api-version=${API_VERSION}`, {
30
+ method: 'POST',
31
+ headers: authHeaders(creds.pat),
32
+ body: JSON.stringify({ query: "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project ORDER BY [System.ChangedDate] DESC" }),
33
+ signal: AbortSignal.timeout(10_000),
34
+ });
35
+ if (res.status === 401)
36
+ return { ok: false, error: 'Invalid PAT or insufficient permissions.' };
37
+ if (res.status === 404)
38
+ return { ok: false, error: `Organization "${creds.organization}" or project "${creds.project}" not found.` };
39
+ if (!res.ok)
40
+ return { ok: false, error: `Azure DevOps returned ${res.status}.` };
41
+ return { ok: true, login: `${creds.organization}/${creds.project}` };
42
+ }
43
+ catch (err) {
44
+ return { ok: false, error: err?.message ?? 'Could not reach Azure DevOps.' };
45
+ }
46
+ }
47
+ async function listRecentWorkItems(creds, types = ['Bug', 'User Story', 'Task'], limit = 20) {
48
+ await (0, core_1.assertResolvesToPublicAddress)('https://dev.azure.com');
49
+ const base = apiBase(creds);
50
+ const typeFilter = types.map((t) => `'${t}'`).join(', ');
51
+ const wiqlRes = await fetch(`${base}/wit/wiql?api-version=${API_VERSION}`, {
52
+ method: 'POST',
53
+ headers: authHeaders(creds.pat),
54
+ body: JSON.stringify({
55
+ query: `SELECT [System.Id],[System.Title],[System.WorkItemType],[System.State] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.WorkItemType] IN (${typeFilter}) ORDER BY [System.ChangedDate] DESC`,
56
+ }),
57
+ signal: AbortSignal.timeout(15_000),
58
+ });
59
+ if (!wiqlRes.ok)
60
+ throw new Error(`Azure DevOps WIQL error: ${wiqlRes.status}`);
61
+ const wiql = (await wiqlRes.json());
62
+ const ids = (wiql.workItems ?? []).slice(0, limit).map((w) => w.id);
63
+ if (ids.length === 0)
64
+ return [];
65
+ const itemsRes = await fetch(`${base}/wit/workitems?ids=${ids.join(',')}&fields=System.Id,System.Title,System.WorkItemType,System.State&api-version=${API_VERSION}`, { headers: authHeaders(creds.pat), signal: AbortSignal.timeout(15_000) });
66
+ if (!itemsRes.ok)
67
+ throw new Error(`Azure DevOps work items fetch error: ${itemsRes.status}`);
68
+ const items = (await itemsRes.json());
69
+ return (items.value ?? []).map((w) => ({
70
+ id: w.id,
71
+ title: w.fields?.['System.Title'] ?? '',
72
+ type: w.fields?.['System.WorkItemType'] ?? '',
73
+ state: w.fields?.['System.State'] ?? '',
74
+ url: `https://dev.azure.com/${creds.organization}/${creds.project}/_workitems/edit/${w.id}`,
75
+ }));
76
+ }
77
+ async function createBug(creds, title, description, priority = 2) {
78
+ await (0, core_1.assertResolvesToPublicAddress)('https://dev.azure.com');
79
+ const base = apiBase(creds);
80
+ const patchOps = [
81
+ { op: 'add', path: '/fields/System.Title', value: title },
82
+ { op: 'add', path: '/fields/Microsoft.VSTS.TCM.ReproSteps', value: description },
83
+ { op: 'add', path: '/fields/Microsoft.VSTS.Common.Priority', value: priority },
84
+ ];
85
+ if (creds.areaPath) {
86
+ patchOps.push({ op: 'add', path: '/fields/System.AreaPath', value: creds.areaPath });
87
+ }
88
+ const res = await fetch(`${base}/wit/workitems/$Bug?api-version=${API_VERSION}`, {
89
+ method: 'POST',
90
+ headers: { ...authHeaders(creds.pat), 'Content-Type': 'application/json-patch+json' },
91
+ body: JSON.stringify(patchOps),
92
+ signal: AbortSignal.timeout(15_000),
93
+ });
94
+ if (!res.ok)
95
+ throw new Error(`Azure DevOps create bug error: ${res.status}`);
96
+ const item = (await res.json());
97
+ return {
98
+ id: item.id,
99
+ title: item.fields?.['System.Title'] ?? title,
100
+ url: `https://dev.azure.com/${creds.organization}/${creds.project}/_workitems/edit/${item.id}`,
101
+ };
102
+ }
103
+ async function listPipelines(creds, limit = 10) {
104
+ await (0, core_1.assertResolvesToPublicAddress)('https://dev.azure.com');
105
+ const base = apiBase(creds);
106
+ const res = await fetch(`${base}/pipelines?api-version=${API_VERSION}&$top=${limit}`, { headers: authHeaders(creds.pat), signal: AbortSignal.timeout(15_000) });
107
+ if (!res.ok)
108
+ throw new Error(`Azure DevOps pipelines error: ${res.status}`);
109
+ const data = (await res.json());
110
+ return (data.value ?? []).map((p) => ({
111
+ id: p.id,
112
+ name: p.name,
113
+ status: p._links?.web ? 'active' : 'unknown',
114
+ url: p._links?.web?.href ?? `https://dev.azure.com/${creds.organization}/${creds.project}/_build?definitionId=${p.id}`,
115
+ }));
116
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Confluence connector — reads pages for requirements-to-test generation.
3
+ * Same credential shape as JIRA (Atlassian unified auth).
4
+ */
5
+ export interface ConfluenceCredentials {
6
+ baseUrl: string;
7
+ email: string;
8
+ apiToken: string;
9
+ password?: string;
10
+ spaceKey?: string;
11
+ bearerToken?: string;
12
+ proxyUrl?: string;
13
+ }
14
+ export declare function testConnection(creds: ConfluenceCredentials): Promise<{
15
+ ok: boolean;
16
+ error?: string;
17
+ }>;
18
+ export declare function listSpaces(creds: ConfluenceCredentials): Promise<any>;
19
+ export declare function listPages(creds: ConfluenceCredentials, spaceKey?: string, limit?: number): Promise<any>;
20
+ export declare function getPageContent(creds: ConfluenceCredentials, pageId: string): Promise<string>;
21
+ /** Full-text search across Confluence — finds pages that mention a screen URL or name. */
22
+ export declare function searchPages(creds: ConfluenceCredentials, text: string, limit?: number): Promise<any>;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * Confluence connector — reads pages for requirements-to-test generation.
4
+ * Same credential shape as JIRA (Atlassian unified auth).
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.testConnection = testConnection;
8
+ exports.listSpaces = listSpaces;
9
+ exports.listPages = listPages;
10
+ exports.getPageContent = getPageContent;
11
+ exports.searchPages = searchPages;
12
+ const core_1 = require("@taodigital/core");
13
+ function validateProxyUrl(proxyUrl) {
14
+ let parsed;
15
+ try {
16
+ parsed = new URL(proxyUrl);
17
+ }
18
+ catch {
19
+ throw new Error(`Invalid proxy URL: ${proxyUrl}`);
20
+ }
21
+ const h = parsed.hostname.toLowerCase();
22
+ if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '169.254.169.254') {
23
+ throw new Error('Proxy URL must not target localhost or metadata addresses');
24
+ }
25
+ }
26
+ async function makeFetch(creds) {
27
+ if (creds.proxyUrl) {
28
+ validateProxyUrl(creds.proxyUrl);
29
+ const { ProxyAgent } = await import('undici');
30
+ const dispatcher = new ProxyAgent(creds.proxyUrl);
31
+ return (url, init) => fetch(url, { ...(init ?? {}), dispatcher });
32
+ }
33
+ return fetch;
34
+ }
35
+ function authHeader(creds) {
36
+ if (creds.bearerToken)
37
+ return `Bearer ${creds.bearerToken}`;
38
+ const secret = creds.password || creds.apiToken;
39
+ return 'Basic ' + Buffer.from(`${creds.email}:${secret}`).toString('base64');
40
+ }
41
+ async function testConnection(creds) {
42
+ try {
43
+ const doFetch = await makeFetch(creds);
44
+ if (!creds.proxyUrl)
45
+ await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
46
+ const res = await doFetch(`${creds.baseUrl}/wiki/rest/api/user/current`, {
47
+ headers: { Authorization: authHeader(creds), Accept: 'application/json' },
48
+ });
49
+ if (!res.ok)
50
+ return { ok: false, error: `Confluence returned ${res.status}` };
51
+ return { ok: true };
52
+ }
53
+ catch (err) {
54
+ return { ok: false, error: String(err) };
55
+ }
56
+ }
57
+ async function listSpaces(creds) {
58
+ await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
59
+ const { getSystemConfig } = await import('@taodigital/core');
60
+ const cfg = await getSystemConfig('connectors_confluence');
61
+ const spacesLimit = parseInt(String(cfg?.spacesLimit ?? '50'), 10);
62
+ const res = await fetch(`${creds.baseUrl}/wiki/rest/api/space?limit=${spacesLimit}`, {
63
+ headers: { Authorization: authHeader(creds), Accept: 'application/json' },
64
+ });
65
+ if (!res.ok)
66
+ throw new Error(`Confluence error: ${res.status}`);
67
+ const data = (await res.json());
68
+ return (data.results ?? []).map((s) => ({ key: s.key, name: s.name }));
69
+ }
70
+ async function listPages(creds, spaceKey, limit) {
71
+ await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
72
+ const { getSystemConfig } = await import('@taodigital/core');
73
+ const cfg = await getSystemConfig('connectors_confluence');
74
+ const resolvedLimit = limit ?? parseInt(String(cfg?.pagesLimit ?? '25'), 10);
75
+ const space = spaceKey ?? creds.spaceKey;
76
+ const url = space
77
+ ? `${creds.baseUrl}/wiki/rest/api/content?spaceKey=${space}&limit=${resolvedLimit}&type=page`
78
+ : `${creds.baseUrl}/wiki/rest/api/content?limit=${resolvedLimit}&type=page`;
79
+ const res = await fetch(url, {
80
+ headers: { Authorization: authHeader(creds), Accept: 'application/json' },
81
+ });
82
+ if (!res.ok)
83
+ throw new Error(`Confluence error: ${res.status}`);
84
+ const data = (await res.json());
85
+ return (data.results ?? []).map((p) => ({ id: p.id, title: p.title }));
86
+ }
87
+ async function getPageContent(creds, pageId) {
88
+ await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
89
+ const res = await fetch(`${creds.baseUrl}/wiki/rest/api/content/${pageId}?expand=body.storage`, { headers: { Authorization: authHeader(creds), Accept: 'application/json' } });
90
+ if (!res.ok)
91
+ throw new Error(`Confluence error: ${res.status}`);
92
+ const page = (await res.json());
93
+ // Strip HTML tags for plain text consumption by the AI.
94
+ return (page.body?.storage?.value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
95
+ }
96
+ /** Full-text search across Confluence — finds pages that mention a screen URL or name. */
97
+ async function searchPages(creds, text, limit) {
98
+ await (0, core_1.assertResolvesToPublicAddress)(creds.baseUrl);
99
+ const { getSystemConfig } = await import('@taodigital/core');
100
+ const cfg = await getSystemConfig('connectors_confluence');
101
+ const resolvedLimit = limit ?? parseInt(String(cfg?.searchLimit ?? '5'), 10);
102
+ const safeText = text.replace(/["\\]/g, ' ').trim();
103
+ const spaceClause = creds.spaceKey ? ` AND space = "${creds.spaceKey}"` : '';
104
+ const cql = `type = page AND text ~ "${safeText}"${spaceClause} ORDER BY lastModified DESC`;
105
+ const res = await fetch(`${creds.baseUrl}/wiki/rest/api/content/search?cql=${encodeURIComponent(cql)}&limit=${resolvedLimit}`, { headers: { Authorization: authHeader(creds), Accept: 'application/json' } });
106
+ if (!res.ok)
107
+ return [];
108
+ const data = (await res.json());
109
+ return (data.results ?? []).map((p) => ({
110
+ id: p.id,
111
+ title: p.title,
112
+ url: `${creds.baseUrl}/wiki${p._links?.webui ?? ''}`,
113
+ }));
114
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Security-focused tests for all connectors.
3
+ * Tests SSRF guard, missing credentials, and happy-path API call shape.
4
+ * No real network calls are made — fetch is mocked globally.
5
+ *
6
+ * Requires DATABASE_URL and MIGRATION_DATABASE_URL env vars to satisfy Prisma's
7
+ * constructor validation (triggered transitively via @taodigital/core → @taodigital/database).
8
+ * The test script in package.json sets mock values for these.
9
+ */
10
+ export {};