@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.
- 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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ServiceNow connector — incident and change management integration.
|
|
4
|
+
* Credentials stored encrypted per project.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.testConnection = testConnection;
|
|
8
|
+
exports.createIncident = createIncident;
|
|
9
|
+
exports.createChangeRequest = createChangeRequest;
|
|
10
|
+
exports.listIncidents = listIncidents;
|
|
11
|
+
const core_1 = require("@taodigital/core");
|
|
12
|
+
function authHeader(creds) {
|
|
13
|
+
return 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
14
|
+
}
|
|
15
|
+
function headers(creds) {
|
|
16
|
+
return {
|
|
17
|
+
Authorization: authHeader(creds),
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
Accept: 'application/json',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function testConnection(creds) {
|
|
23
|
+
try {
|
|
24
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
25
|
+
const res = await fetch(`${creds.instanceUrl}/api/now/table/sys_user?sysparm_limit=1`, {
|
|
26
|
+
headers: headers(creds),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
return { ok: false, error: `ServiceNow returned ${res.status}` };
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return { ok: false, error: String(err) };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function createIncident(creds, opts) {
|
|
37
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
38
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
39
|
+
const snCfg = await getSystemConfig('connectors_servicenow');
|
|
40
|
+
const res = await fetch(`${creds.instanceUrl}/api/now/table/incident`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: headers(creds),
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
short_description: opts.shortDescription,
|
|
45
|
+
description: opts.description,
|
|
46
|
+
urgency: String(opts.urgency ?? opts.defaultUrgency ?? 2),
|
|
47
|
+
impact: String(opts.impact ?? opts.defaultImpact ?? 2),
|
|
48
|
+
category: opts.category ?? opts.defaultCategory ?? String(snCfg?.defaultCategory ?? ''),
|
|
49
|
+
caller_id: creds.username,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
throw new Error(`ServiceNow error: ${res.status} ${await res.text()}`);
|
|
54
|
+
const data = (await res.json());
|
|
55
|
+
return {
|
|
56
|
+
sysId: data.result?.sys_id,
|
|
57
|
+
number: data.result?.number,
|
|
58
|
+
url: `${creds.instanceUrl}/nav_to.do?uri=incident.do?sys_id=${data.result?.sys_id}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function createChangeRequest(creds, opts) {
|
|
62
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
63
|
+
const res = await fetch(`${creds.instanceUrl}/api/now/table/change_request`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: headers(creds),
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
short_description: opts.shortDescription,
|
|
68
|
+
description: opts.description,
|
|
69
|
+
type: opts.type ?? opts.defaultType ?? 'normal',
|
|
70
|
+
requested_by: creds.username,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok)
|
|
74
|
+
throw new Error(`ServiceNow error: ${res.status}`);
|
|
75
|
+
const data = (await res.json());
|
|
76
|
+
return { sysId: data.result?.sys_id, number: data.result?.number };
|
|
77
|
+
}
|
|
78
|
+
async function listIncidents(creds, opts) {
|
|
79
|
+
await (0, core_1.assertResolvesToPublicAddress)(creds.instanceUrl);
|
|
80
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
81
|
+
const snCfg = await getSystemConfig('connectors_servicenow');
|
|
82
|
+
const pageLimit = parseInt(String(snCfg?.pageLimit ?? '25'), 10);
|
|
83
|
+
const params = new URLSearchParams({
|
|
84
|
+
sysparm_limit: String(opts?.limit ?? pageLimit),
|
|
85
|
+
sysparm_fields: 'sys_id,number,short_description,state,urgency,priority,opened_at,caller_id',
|
|
86
|
+
sysparm_display_value: 'true',
|
|
87
|
+
...(opts?.state ? { sysparm_query: `state=${opts.state}` } : {}),
|
|
88
|
+
});
|
|
89
|
+
const res = await fetch(`${creds.instanceUrl}/api/now/table/incident?${params}`, {
|
|
90
|
+
headers: headers(creds),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok)
|
|
93
|
+
throw new Error(`ServiceNow error: ${res.status}`);
|
|
94
|
+
const data = (await res.json());
|
|
95
|
+
return (data.result ?? []);
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taodigital/connectors",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public",
|
|
7
|
+
"registry": "https://registry.npmjs.org/"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "DATABASE_URL=postgresql://mock:mock@localhost:5432/mock MIGRATION_DATABASE_URL=postgresql://mock:mock@localhost:5432/mock tsx --test src/**/*.test.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@taodigital/core": "*"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
|
|
8
|
+
import { assertResolvesToPublicAddress } from '@taodigital/core';
|
|
9
|
+
|
|
10
|
+
export interface AzureDevOpsCredentials {
|
|
11
|
+
organization: string; // Azure DevOps org name
|
|
12
|
+
project: string; // project name or ID
|
|
13
|
+
pat: string; // Personal Access Token
|
|
14
|
+
areaPath?: string; // default work item area path (optional)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const API_VERSION = '7.1';
|
|
18
|
+
|
|
19
|
+
function apiBase(creds: AzureDevOpsCredentials): string {
|
|
20
|
+
return `https://dev.azure.com/${encodeURIComponent(creds.organization)}/${encodeURIComponent(creds.project)}/_apis`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function authHeaders(pat: string): Record<string, string> {
|
|
24
|
+
const encoded = Buffer.from(`:${pat}`).toString('base64');
|
|
25
|
+
return {
|
|
26
|
+
Authorization: `Basic ${encoded}`,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function testConnection(
|
|
32
|
+
creds: AzureDevOpsCredentials,
|
|
33
|
+
): Promise<{ ok: boolean; login?: string; error?: string }> {
|
|
34
|
+
try {
|
|
35
|
+
await assertResolvesToPublicAddress('https://dev.azure.com');
|
|
36
|
+
const base = apiBase(creds);
|
|
37
|
+
const res = await fetch(
|
|
38
|
+
`${base}/wit/wiql?api-version=${API_VERSION}`,
|
|
39
|
+
{
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: authHeaders(creds.pat),
|
|
42
|
+
body: JSON.stringify({ query: "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project ORDER BY [System.ChangedDate] DESC" }),
|
|
43
|
+
signal: AbortSignal.timeout(10_000),
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
if (res.status === 401) return { ok: false, error: 'Invalid PAT or insufficient permissions.' };
|
|
47
|
+
if (res.status === 404) return { ok: false, error: `Organization "${creds.organization}" or project "${creds.project}" not found.` };
|
|
48
|
+
if (!res.ok) return { ok: false, error: `Azure DevOps returned ${res.status}.` };
|
|
49
|
+
return { ok: true, login: `${creds.organization}/${creds.project}` };
|
|
50
|
+
} catch (err: any) {
|
|
51
|
+
return { ok: false, error: err?.message ?? 'Could not reach Azure DevOps.' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function listRecentWorkItems(
|
|
56
|
+
creds: AzureDevOpsCredentials,
|
|
57
|
+
types: string[] = ['Bug', 'User Story', 'Task'],
|
|
58
|
+
limit = 20,
|
|
59
|
+
): Promise<Array<{ id: number; title: string; type: string; state: string; url: string }>> {
|
|
60
|
+
await assertResolvesToPublicAddress('https://dev.azure.com');
|
|
61
|
+
const base = apiBase(creds);
|
|
62
|
+
const typeFilter = types.map((t) => `'${t}'`).join(', ');
|
|
63
|
+
const wiqlRes = await fetch(`${base}/wit/wiql?api-version=${API_VERSION}`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: authHeaders(creds.pat),
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
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`,
|
|
68
|
+
}),
|
|
69
|
+
signal: AbortSignal.timeout(15_000),
|
|
70
|
+
});
|
|
71
|
+
if (!wiqlRes.ok) throw new Error(`Azure DevOps WIQL error: ${wiqlRes.status}`);
|
|
72
|
+
const wiql = (await wiqlRes.json()) as any;
|
|
73
|
+
const ids: number[] = (wiql.workItems ?? []).slice(0, limit).map((w: any) => w.id);
|
|
74
|
+
if (ids.length === 0) return [];
|
|
75
|
+
|
|
76
|
+
const itemsRes = await fetch(
|
|
77
|
+
`${base}/wit/workitems?ids=${ids.join(',')}&fields=System.Id,System.Title,System.WorkItemType,System.State&api-version=${API_VERSION}`,
|
|
78
|
+
{ headers: authHeaders(creds.pat), signal: AbortSignal.timeout(15_000) },
|
|
79
|
+
);
|
|
80
|
+
if (!itemsRes.ok) throw new Error(`Azure DevOps work items fetch error: ${itemsRes.status}`);
|
|
81
|
+
const items = (await itemsRes.json()) as any;
|
|
82
|
+
return (items.value ?? []).map((w: any) => ({
|
|
83
|
+
id: w.id,
|
|
84
|
+
title: w.fields?.['System.Title'] ?? '',
|
|
85
|
+
type: w.fields?.['System.WorkItemType'] ?? '',
|
|
86
|
+
state: w.fields?.['System.State'] ?? '',
|
|
87
|
+
url: `https://dev.azure.com/${creds.organization}/${creds.project}/_workitems/edit/${w.id}`,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createBug(
|
|
92
|
+
creds: AzureDevOpsCredentials,
|
|
93
|
+
title: string,
|
|
94
|
+
description: string,
|
|
95
|
+
priority: 1 | 2 | 3 | 4 = 2,
|
|
96
|
+
): Promise<{ id: number; url: string; title: string }> {
|
|
97
|
+
await assertResolvesToPublicAddress('https://dev.azure.com');
|
|
98
|
+
const base = apiBase(creds);
|
|
99
|
+
const patchOps: any[] = [
|
|
100
|
+
{ op: 'add', path: '/fields/System.Title', value: title },
|
|
101
|
+
{ op: 'add', path: '/fields/Microsoft.VSTS.TCM.ReproSteps', value: description },
|
|
102
|
+
{ op: 'add', path: '/fields/Microsoft.VSTS.Common.Priority', value: priority },
|
|
103
|
+
];
|
|
104
|
+
if (creds.areaPath) {
|
|
105
|
+
patchOps.push({ op: 'add', path: '/fields/System.AreaPath', value: creds.areaPath });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await fetch(`${base}/wit/workitems/$Bug?api-version=${API_VERSION}`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { ...authHeaders(creds.pat), 'Content-Type': 'application/json-patch+json' },
|
|
111
|
+
body: JSON.stringify(patchOps),
|
|
112
|
+
signal: AbortSignal.timeout(15_000),
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) throw new Error(`Azure DevOps create bug error: ${res.status}`);
|
|
115
|
+
const item = (await res.json()) as any;
|
|
116
|
+
return {
|
|
117
|
+
id: item.id,
|
|
118
|
+
title: item.fields?.['System.Title'] ?? title,
|
|
119
|
+
url: `https://dev.azure.com/${creds.organization}/${creds.project}/_workitems/edit/${item.id}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function listPipelines(
|
|
124
|
+
creds: AzureDevOpsCredentials,
|
|
125
|
+
limit = 10,
|
|
126
|
+
): Promise<Array<{ id: number; name: string; status: string; url: string }>> {
|
|
127
|
+
await assertResolvesToPublicAddress('https://dev.azure.com');
|
|
128
|
+
const base = apiBase(creds);
|
|
129
|
+
const res = await fetch(
|
|
130
|
+
`${base}/pipelines?api-version=${API_VERSION}&$top=${limit}`,
|
|
131
|
+
{ headers: authHeaders(creds.pat), signal: AbortSignal.timeout(15_000) },
|
|
132
|
+
);
|
|
133
|
+
if (!res.ok) throw new Error(`Azure DevOps pipelines error: ${res.status}`);
|
|
134
|
+
const data = (await res.json()) as any;
|
|
135
|
+
return (data.value ?? []).map((p: any) => ({
|
|
136
|
+
id: p.id,
|
|
137
|
+
name: p.name,
|
|
138
|
+
status: p._links?.web ? 'active' : 'unknown',
|
|
139
|
+
url: p._links?.web?.href ?? `https://dev.azure.com/${creds.organization}/${creds.project}/_build?definitionId=${p.id}`,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confluence connector — reads pages for requirements-to-test generation.
|
|
3
|
+
* Same credential shape as JIRA (Atlassian unified auth).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { assertResolvesToPublicAddress } from '@taodigital/core';
|
|
7
|
+
|
|
8
|
+
export interface ConfluenceCredentials {
|
|
9
|
+
baseUrl: string; // e.g. https://yourcompany.atlassian.net or https://api.atlassian.com/ex/confluence/{cloudId}
|
|
10
|
+
email: string;
|
|
11
|
+
apiToken: string;
|
|
12
|
+
password?: string; // Server / Data Center — takes priority over apiToken
|
|
13
|
+
spaceKey?: string; // e.g. "QA" or "PROD"
|
|
14
|
+
bearerToken?: string; // Atlassian OAuth 2.0 token — same app as Jira
|
|
15
|
+
proxyUrl?: string; // e.g. http://corporate-proxy:8080 — enables on-prem Confluence, skips SSRF guard
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateProxyUrl(proxyUrl: string): void {
|
|
19
|
+
let parsed: URL;
|
|
20
|
+
try { parsed = new URL(proxyUrl); } catch { throw new Error(`Invalid proxy URL: ${proxyUrl}`); }
|
|
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
|
+
|
|
27
|
+
async function makeFetch(creds: ConfluenceCredentials): Promise<typeof fetch> {
|
|
28
|
+
if (creds.proxyUrl) {
|
|
29
|
+
validateProxyUrl(creds.proxyUrl);
|
|
30
|
+
const { ProxyAgent } = await import('undici');
|
|
31
|
+
const dispatcher = new ProxyAgent(creds.proxyUrl);
|
|
32
|
+
return (url, init) => fetch(url, { ...(init ?? {}), dispatcher } as any);
|
|
33
|
+
}
|
|
34
|
+
return fetch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function authHeader(creds: ConfluenceCredentials): string {
|
|
38
|
+
if (creds.bearerToken) return `Bearer ${creds.bearerToken}`;
|
|
39
|
+
const secret = creds.password || creds.apiToken;
|
|
40
|
+
return 'Basic ' + Buffer.from(`${creds.email}:${secret}`).toString('base64');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function testConnection(creds: ConfluenceCredentials): Promise<{ ok: boolean; error?: string }> {
|
|
44
|
+
try {
|
|
45
|
+
const doFetch = await makeFetch(creds);
|
|
46
|
+
if (!creds.proxyUrl) await assertResolvesToPublicAddress(creds.baseUrl);
|
|
47
|
+
const res = await doFetch(`${creds.baseUrl}/wiki/rest/api/user/current`, {
|
|
48
|
+
headers: { Authorization: authHeader(creds), Accept: 'application/json' },
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) return { ok: false, error: `Confluence returned ${res.status}` };
|
|
51
|
+
return { ok: true };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, error: String(err) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function listSpaces(creds: ConfluenceCredentials) {
|
|
58
|
+
await 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) throw new Error(`Confluence error: ${res.status}`);
|
|
66
|
+
const data = (await res.json()) as any;
|
|
67
|
+
return (data.results ?? []).map((s: any) => ({ key: s.key, name: s.name }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function listPages(creds: ConfluenceCredentials, spaceKey?: string, limit?: number) {
|
|
71
|
+
await 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
|
+
|
|
80
|
+
const res = await fetch(url, {
|
|
81
|
+
headers: { Authorization: authHeader(creds), Accept: 'application/json' },
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) throw new Error(`Confluence error: ${res.status}`);
|
|
84
|
+
const data = (await res.json()) as any;
|
|
85
|
+
return (data.results ?? []).map((p: any) => ({ id: p.id, title: p.title }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function getPageContent(creds: ConfluenceCredentials, pageId: string): Promise<string> {
|
|
89
|
+
await assertResolvesToPublicAddress(creds.baseUrl);
|
|
90
|
+
const res = await fetch(
|
|
91
|
+
`${creds.baseUrl}/wiki/rest/api/content/${pageId}?expand=body.storage`,
|
|
92
|
+
{ headers: { Authorization: authHeader(creds), Accept: 'application/json' } }
|
|
93
|
+
);
|
|
94
|
+
if (!res.ok) throw new Error(`Confluence error: ${res.status}`);
|
|
95
|
+
const page = (await res.json()) as any;
|
|
96
|
+
// Strip HTML tags for plain text consumption by the AI.
|
|
97
|
+
return (page.body?.storage?.value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Full-text search across Confluence — finds pages that mention a screen URL or name. */
|
|
101
|
+
export async function searchPages(creds: ConfluenceCredentials, text: string, limit?: number) {
|
|
102
|
+
await assertResolvesToPublicAddress(creds.baseUrl);
|
|
103
|
+
const { getSystemConfig } = await import('@taodigital/core');
|
|
104
|
+
const cfg = await getSystemConfig('connectors_confluence');
|
|
105
|
+
const resolvedLimit = limit ?? parseInt(String(cfg?.searchLimit ?? '5'), 10);
|
|
106
|
+
const safeText = text.replace(/["\\]/g, ' ').trim();
|
|
107
|
+
const spaceClause = creds.spaceKey ? ` AND space = "${creds.spaceKey}"` : '';
|
|
108
|
+
const cql = `type = page AND text ~ "${safeText}"${spaceClause} ORDER BY lastModified DESC`;
|
|
109
|
+
const res = await fetch(
|
|
110
|
+
`${creds.baseUrl}/wiki/rest/api/content/search?cql=${encodeURIComponent(cql)}&limit=${resolvedLimit}`,
|
|
111
|
+
{ headers: { Authorization: authHeader(creds), Accept: 'application/json' } }
|
|
112
|
+
);
|
|
113
|
+
if (!res.ok) return [];
|
|
114
|
+
const data = (await res.json()) as any;
|
|
115
|
+
return (data.results ?? []).map((p: any) => ({
|
|
116
|
+
id: p.id,
|
|
117
|
+
title: p.title,
|
|
118
|
+
url: `${creds.baseUrl}/wiki${p._links?.webui ?? ''}`,
|
|
119
|
+
}));
|
|
120
|
+
}
|