@credal/actions 0.2.222 → 0.2.224
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/actions/actionMapper.js +66 -1
- package/dist/actions/autogen/templates.d.ts +9 -0
- package/dist/actions/autogen/templates.js +920 -0
- package/dist/actions/autogen/types.d.ts +893 -1
- package/dist/actions/autogen/types.js +353 -0
- package/dist/actions/groups.js +3 -31
- package/dist/actions/providers/microsoft/getSharepointItem.d.ts +3 -0
- package/dist/actions/providers/microsoft/getSharepointItem.js +61 -0
- package/dist/actions/providers/microsoft/listSharepointFolder.d.ts +3 -0
- package/dist/actions/providers/microsoft/listSharepointFolder.js +92 -0
- package/dist/actions/providers/microsoft/readSharepointContent.d.ts +3 -0
- package/dist/actions/providers/microsoft/readSharepointContent.js +157 -0
- package/dist/actions/providers/microsoft/searchSharepoint.d.ts +3 -0
- package/dist/actions/providers/microsoft/searchSharepoint.js +147 -0
- package/dist/actions/providers/microsoft/sharepointUtils.d.ts +56 -0
- package/dist/actions/providers/microsoft/sharepointUtils.js +153 -0
- package/dist/actions/providers/microsoft/utils.d.ts +1 -0
- package/dist/actions/providers/microsoft/utils.js +1 -0
- package/dist/actions/providers/servicenow/getChangeRequests.d.ts +3 -0
- package/dist/actions/providers/servicenow/getChangeRequests.js +56 -0
- package/dist/actions/providers/servicenow/getIncidents.d.ts +3 -0
- package/dist/actions/providers/servicenow/getIncidents.js +58 -0
- package/dist/actions/providers/servicenow/getRecordsByQuery.d.ts +3 -0
- package/dist/actions/providers/servicenow/getRecordsByQuery.js +42 -0
- package/dist/actions/providers/servicenow/getSCTasks.d.ts +3 -0
- package/dist/actions/providers/servicenow/getSCTasks.js +53 -0
- package/dist/actions/providers/servicenow/getVIPTickets.d.ts +3 -0
- package/dist/actions/providers/servicenow/getVIPTickets.js +61 -0
- package/dist/actions/providers/servicenow/utils/tableQuery.d.ts +22 -0
- package/dist/actions/providers/servicenow/utils/tableQuery.js +62 -0
- package/package.json +1 -1
- package/dist/actions/providers/credal/callCopilot.d.ts +0 -3
- package/dist/actions/providers/credal/callCopilot.js +0 -36
- package/dist/actions/providers/math/index.d.ts +0 -1
- package/dist/actions/providers/math/index.js +0 -37
- package/dist/actions/providers/salesforce/getSalesforceRecordByQuery.d.ts +0 -3
- package/dist/actions/providers/salesforce/getSalesforceRecordByQuery.js +0 -43
- package/dist/actions/providers/slack/index.d.ts +0 -1
- package/dist/actions/providers/slack/index.js +0 -37
- package/dist/actions/providers/slack/listConversations.d.ts +0 -3
- package/dist/actions/providers/slack/listConversations.js +0 -41
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import mammoth from "mammoth";
|
|
2
|
+
import officeParser from "officeparser";
|
|
3
|
+
import { MISSING_AUTH_TOKEN } from "../../util/missingAuthConstants.js";
|
|
4
|
+
import { ApiError, axiosClient } from "../../util/axiosClient.js";
|
|
5
|
+
import { extractTextFromPdf } from "../../../utils/pdf.js";
|
|
6
|
+
import { parseWorkbookBufferToPlainText } from "../../../utils/google.js";
|
|
7
|
+
import { MICROSOFT_GRAPH_API_URL } from "./utils.js";
|
|
8
|
+
import { MISSING_SITES_SCOPE_MESSAGE, resolveItemFromUrl } from "./sharepointUtils.js";
|
|
9
|
+
const DEFAULT_FILE_SIZE_LIMIT_MB = 50;
|
|
10
|
+
const TEXT_MIME_TYPES = new Set([
|
|
11
|
+
"text/plain",
|
|
12
|
+
"text/html",
|
|
13
|
+
"text/csv",
|
|
14
|
+
"text/tab-separated-values",
|
|
15
|
+
"application/rtf",
|
|
16
|
+
"application/json",
|
|
17
|
+
]);
|
|
18
|
+
function stripHtml(html) {
|
|
19
|
+
return html
|
|
20
|
+
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ")
|
|
21
|
+
.replace(/<[^>]+>/g, " ")
|
|
22
|
+
.replace(/ /gi, " ")
|
|
23
|
+
.replace(/&/gi, "&")
|
|
24
|
+
.replace(/</gi, "<")
|
|
25
|
+
.replace(/>/gi, ">")
|
|
26
|
+
.replace(/"/gi, '"')
|
|
27
|
+
.replace(/'/gi, "'")
|
|
28
|
+
.replace(/[ \t]+/g, " ")
|
|
29
|
+
.replace(/\s*\n\s*/g, "\n")
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
function buildResult(name, url, content, charLimit) {
|
|
33
|
+
const originalLength = content.length;
|
|
34
|
+
let finalContent = content.trim();
|
|
35
|
+
if (charLimit && charLimit > 0 && finalContent.length > charLimit) {
|
|
36
|
+
finalContent = `${finalContent.slice(0, charLimit)}\n\n[Content truncated to ${charLimit} characters]`;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
results: [{ name, url, contents: { content: finalContent, fileName: name, fileLength: originalLength } }],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function convertFileBuffer(mimeType, buffer, charLimit) {
|
|
44
|
+
if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
|
|
45
|
+
mimeType === "application/msword") {
|
|
46
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
47
|
+
return result.value ?? "";
|
|
48
|
+
}
|
|
49
|
+
if (mimeType === "application/pdf") {
|
|
50
|
+
return await extractTextFromPdf(buffer);
|
|
51
|
+
}
|
|
52
|
+
if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
|
|
53
|
+
mimeType === "application/vnd.ms-excel") {
|
|
54
|
+
return parseWorkbookBufferToPlainText(buffer, charLimit);
|
|
55
|
+
}
|
|
56
|
+
if (mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation") {
|
|
57
|
+
return await officeParser.parseOfficeAsync(buffer);
|
|
58
|
+
}
|
|
59
|
+
if (TEXT_MIME_TYPES.has(mimeType) || mimeType.startsWith("text/")) {
|
|
60
|
+
return buffer.toString("utf-8");
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Unsupported file type: ${mimeType}`);
|
|
63
|
+
}
|
|
64
|
+
async function readFileContent(file, headers, charLimit, fileSizeLimit) {
|
|
65
|
+
const maxMegabytes = fileSizeLimit && fileSizeLimit > 0 ? fileSizeLimit : DEFAULT_FILE_SIZE_LIMIT_MB;
|
|
66
|
+
const maxFileSize = maxMegabytes * 1024 * 1024;
|
|
67
|
+
if (file.sizeBytes !== undefined && file.sizeBytes > maxFileSize) {
|
|
68
|
+
const actualMegabytes = (file.sizeBytes / (1024 * 1024)).toFixed(1);
|
|
69
|
+
return { success: false, error: `File too large: ${actualMegabytes}MB exceeds the ${maxMegabytes}MB limit` };
|
|
70
|
+
}
|
|
71
|
+
const downloadResponse = await axiosClient.get(`${MICROSOFT_GRAPH_API_URL}/drives/${file.driveId}/items/${file.itemId}/content`, { headers, responseType: "arraybuffer" });
|
|
72
|
+
const buffer = Buffer.from(downloadResponse.data);
|
|
73
|
+
const content = await convertFileBuffer(file.mimeType ?? "", buffer, charLimit);
|
|
74
|
+
return buildResult(file.name ?? "", file.webUrl ?? "", content, charLimit);
|
|
75
|
+
}
|
|
76
|
+
async function readPageContent(page, headers, charLimit) {
|
|
77
|
+
if (!page.pageId) {
|
|
78
|
+
return { success: false, error: "Could not identify the site page from the URL" };
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const response = await axiosClient.get(`${MICROSOFT_GRAPH_API_URL}/sites/${page.siteId}/pages/${page.pageId}/microsoft.graph.sitePage/webparts`, { headers });
|
|
82
|
+
const webparts = response.data.value ?? [];
|
|
83
|
+
const content = webparts
|
|
84
|
+
.map(webpart => (webpart.innerHtml ? stripHtml(webpart.innerHtml) : ""))
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join("\n\n");
|
|
87
|
+
return buildResult(page.name ?? "", page.webUrl ?? "", content, charLimit);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
|
|
91
|
+
return { success: false, error: MISSING_SITES_SCOPE_MESSAGE };
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the target document from either a driveItem reference or a URL. The driveItem
|
|
98
|
+
* takes precedence over url when both are provided (it is an exact round-trip token; a
|
|
99
|
+
* url would be re-resolved and could name a different item).
|
|
100
|
+
*/
|
|
101
|
+
async function resolveTargetItem(authToken, params, headers) {
|
|
102
|
+
const { url, driveItem } = params;
|
|
103
|
+
if (driveItem) {
|
|
104
|
+
const { driveId, itemId } = driveItem;
|
|
105
|
+
const metadataResponse = await axiosClient.get(`${MICROSOFT_GRAPH_API_URL}/drives/${driveId}/items/${itemId}`, {
|
|
106
|
+
headers,
|
|
107
|
+
});
|
|
108
|
+
const item = metadataResponse.data;
|
|
109
|
+
return {
|
|
110
|
+
itemType: item.folder ? "folder" : "file",
|
|
111
|
+
driveId,
|
|
112
|
+
itemId,
|
|
113
|
+
name: item.name,
|
|
114
|
+
webUrl: item.webUrl,
|
|
115
|
+
mimeType: item.file?.mimeType,
|
|
116
|
+
sizeBytes: item.size,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return resolveItemFromUrl(authToken, url);
|
|
120
|
+
}
|
|
121
|
+
const readSharepointContent = async ({ params, authParams, }) => {
|
|
122
|
+
if (!authParams.authToken) {
|
|
123
|
+
return { success: false, error: MISSING_AUTH_TOKEN };
|
|
124
|
+
}
|
|
125
|
+
const { url, driveItem, charLimit, fileSizeLimit } = params;
|
|
126
|
+
if (charLimit != null && (!Number.isFinite(charLimit) || charLimit <= 0)) {
|
|
127
|
+
return { success: false, error: "charLimit must be a positive number" };
|
|
128
|
+
}
|
|
129
|
+
if (fileSizeLimit != null && (!Number.isFinite(fileSizeLimit) || fileSizeLimit <= 0)) {
|
|
130
|
+
return { success: false, error: "fileSizeLimit must be a positive number" };
|
|
131
|
+
}
|
|
132
|
+
if (!url && !driveItem) {
|
|
133
|
+
return { success: false, error: "Either url or driveItem must be provided" };
|
|
134
|
+
}
|
|
135
|
+
const headers = { Authorization: `Bearer ${authParams.authToken}` };
|
|
136
|
+
try {
|
|
137
|
+
const resolved = await resolveTargetItem(authParams.authToken, { url, driveItem }, headers);
|
|
138
|
+
if (resolved.itemType === "folder" || resolved.itemType === "site") {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: `The URL points to a ${resolved.itemType}, not a document. Use listSharepointFolder to enumerate it and read files individually.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (resolved.itemType === "page") {
|
|
145
|
+
return await readPageContent(resolved, headers, charLimit);
|
|
146
|
+
}
|
|
147
|
+
return await readFileContent(resolved, headers, charLimit, fileSizeLimit);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error("Error reading SharePoint content", error);
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
export default readSharepointContent;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { MISSING_AUTH_TOKEN } from "../../util/missingAuthConstants.js";
|
|
2
|
+
import { axiosClient } from "../../util/axiosClient.js";
|
|
3
|
+
import { MICROSOFT_GRAPH_API_URL } from "./utils.js";
|
|
4
|
+
import { MISSING_SITES_SCOPE_MESSAGE, isSiteScopeError, resolveItemFromUrl } from "./sharepointUtils.js";
|
|
5
|
+
const DEFAULT_LIMIT = 25;
|
|
6
|
+
const SEARCH_PAGE_SIZE = 100;
|
|
7
|
+
function mapDriveItem(item, snippet) {
|
|
8
|
+
return {
|
|
9
|
+
itemId: item.id ?? "",
|
|
10
|
+
driveId: item.parentReference?.driveId,
|
|
11
|
+
name: item.name ?? "",
|
|
12
|
+
mimeType: item.file?.mimeType,
|
|
13
|
+
webUrl: item.webUrl,
|
|
14
|
+
snippet,
|
|
15
|
+
lastModified: item.lastModifiedDateTime,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function dedupeByItemIdKeepFirst(files) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
return files.filter(file => {
|
|
21
|
+
if (seen.has(file.itemId))
|
|
22
|
+
return false;
|
|
23
|
+
seen.add(file.itemId);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** POST /search/query, optionally KQL path-scoped — requires Sites.Read.All. */
|
|
28
|
+
async function searchWithSearchApi(query, pathScope, limit, headers) {
|
|
29
|
+
const queryString = pathScope ? `${query} path:"${pathScope}"` : query;
|
|
30
|
+
const files = [];
|
|
31
|
+
let truncated = false;
|
|
32
|
+
let from = 0;
|
|
33
|
+
let moreResultsAvailable = false;
|
|
34
|
+
do {
|
|
35
|
+
const response = await axiosClient.post(`${MICROSOFT_GRAPH_API_URL}/search/query`, {
|
|
36
|
+
requests: [
|
|
37
|
+
{
|
|
38
|
+
entityTypes: ["driveItem"],
|
|
39
|
+
query: { queryString },
|
|
40
|
+
from,
|
|
41
|
+
size: SEARCH_PAGE_SIZE,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}, { headers });
|
|
45
|
+
const container = response.data?.value?.[0]?.hitsContainers?.[0];
|
|
46
|
+
const hits = container?.hits ?? [];
|
|
47
|
+
moreResultsAvailable = Boolean(container?.moreResultsAvailable);
|
|
48
|
+
if (hits.length === 0)
|
|
49
|
+
break;
|
|
50
|
+
// Truncation is detected in two places because each sees a case the other can't:
|
|
51
|
+
// here, stopping mid-page with unconsumed hits in hand (moreResultsAvailable can be
|
|
52
|
+
// false then — it only signals more pages on the server, not leftovers in this page);
|
|
53
|
+
for (const hit of hits) {
|
|
54
|
+
if (files.length >= limit) {
|
|
55
|
+
truncated = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
files.push(mapDriveItem(hit.resource ?? {}, hit.summary));
|
|
59
|
+
}
|
|
60
|
+
from += hits.length;
|
|
61
|
+
} while (!truncated && moreResultsAvailable && files.length < limit);
|
|
62
|
+
// and here, the limit exactly consumed the page (no hit was encountered past the
|
|
63
|
+
// limit, so the in-loop check never fired) while the server still has more pages.
|
|
64
|
+
if (files.length >= limit && moreResultsAvailable) {
|
|
65
|
+
truncated = true;
|
|
66
|
+
}
|
|
67
|
+
return { success: true, files: dedupeByItemIdKeepFirst(files), truncated };
|
|
68
|
+
}
|
|
69
|
+
/** Item-scoped GET /drives/{driveId}/items/{itemId}/search — works with Files.Read.All alone. */
|
|
70
|
+
async function searchWithinFolder(driveId, itemId, query, limit, headers) {
|
|
71
|
+
const escapedQuery = encodeURIComponent(query.replace(/'/g, "''"));
|
|
72
|
+
const base = itemId === "root"
|
|
73
|
+
? `${MICROSOFT_GRAPH_API_URL}/drives/${driveId}/root`
|
|
74
|
+
: `${MICROSOFT_GRAPH_API_URL}/drives/${driveId}/items/${itemId}`;
|
|
75
|
+
const files = [];
|
|
76
|
+
let truncated = false;
|
|
77
|
+
let nextUrl = `${base}/search(q='${escapedQuery}')?$top=${SEARCH_PAGE_SIZE}`;
|
|
78
|
+
while (!truncated && nextUrl) {
|
|
79
|
+
const response = await axiosClient.get(nextUrl, { headers });
|
|
80
|
+
const items = response.data.value ?? [];
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
if (files.length >= limit) {
|
|
83
|
+
truncated = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
files.push(mapDriveItem(item));
|
|
87
|
+
}
|
|
88
|
+
nextUrl = response.data["@odata.nextLink"];
|
|
89
|
+
}
|
|
90
|
+
if (files.length >= limit && nextUrl) {
|
|
91
|
+
truncated = true;
|
|
92
|
+
}
|
|
93
|
+
return { success: true, files: dedupeByItemIdKeepFirst(files), truncated };
|
|
94
|
+
}
|
|
95
|
+
const searchSharepoint = async ({ params, authParams, }) => {
|
|
96
|
+
if (!authParams.authToken) {
|
|
97
|
+
return { success: false, error: MISSING_AUTH_TOKEN };
|
|
98
|
+
}
|
|
99
|
+
const { query, scopeUrl } = params;
|
|
100
|
+
if (params.limit != null && (!Number.isFinite(params.limit) || params.limit <= 0)) {
|
|
101
|
+
return { success: false, error: "limit must be a positive number" };
|
|
102
|
+
}
|
|
103
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
104
|
+
const headers = { Authorization: `Bearer ${authParams.authToken}` };
|
|
105
|
+
try {
|
|
106
|
+
if (!scopeUrl) {
|
|
107
|
+
try {
|
|
108
|
+
return await searchWithSearchApi(query, undefined, limit, headers);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (isSiteScopeError(error)) {
|
|
112
|
+
return { success: false, error: MISSING_SITES_SCOPE_MESSAGE };
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Resolving the scopeUrl decides the search mode: folders and library roots use an
|
|
118
|
+
// item-scoped drive search (Files.Read.All alone), sites use the path-filtered Search API
|
|
119
|
+
const resolved = await resolveItemFromUrl(authParams.authToken, scopeUrl);
|
|
120
|
+
if (resolved.itemType === "folder") {
|
|
121
|
+
return await searchWithinFolder(resolved.driveId, resolved.itemId, query, limit, headers);
|
|
122
|
+
}
|
|
123
|
+
if (resolved.itemType === "site") {
|
|
124
|
+
try {
|
|
125
|
+
return await searchWithSearchApi(query, resolved.webUrl ?? scopeUrl, limit, headers);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (isSiteScopeError(error)) {
|
|
129
|
+
return { success: false, error: MISSING_SITES_SCOPE_MESSAGE };
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: `scopeUrl points to a ${resolved.itemType === "page" ? "site page" : "file"} — provide a folder, document library, or site URL`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error("Error searching SharePoint", error);
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
export default searchSharepoint;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const MISSING_SITES_SCOPE_MESSAGE = "Missing Sites.Read.All: this token only permits file/folder access. Site, page, and site-wide search operations require re-authentication with sites scopes.";
|
|
2
|
+
/**
|
|
3
|
+
* Encodes a URL into a Microsoft Graph share token usable with the /shares endpoint:
|
|
4
|
+
* "u!" + unpadded base64url of the URL.
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeShareUrl(url: string): string;
|
|
7
|
+
export type SharepointDriveItem = {
|
|
8
|
+
id?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
webUrl?: string;
|
|
11
|
+
size?: number;
|
|
12
|
+
lastModifiedDateTime?: string;
|
|
13
|
+
file?: {
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
};
|
|
16
|
+
folder?: {
|
|
17
|
+
childCount?: number;
|
|
18
|
+
};
|
|
19
|
+
parentReference?: {
|
|
20
|
+
driveId?: string;
|
|
21
|
+
siteId?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type ResolvedSharepointItem = {
|
|
25
|
+
itemType: "file" | "folder";
|
|
26
|
+
driveId: string;
|
|
27
|
+
itemId: string;
|
|
28
|
+
siteId?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
webUrl?: string;
|
|
31
|
+
mimeType?: string;
|
|
32
|
+
sizeBytes?: number;
|
|
33
|
+
} | {
|
|
34
|
+
itemType: "site";
|
|
35
|
+
siteId: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
webUrl?: string;
|
|
38
|
+
} | {
|
|
39
|
+
itemType: "page";
|
|
40
|
+
siteId: string;
|
|
41
|
+
pageId?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
webUrl?: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function isSiteScopeError(error: unknown): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Resolves any pasted SharePoint/OneDrive URL (file, folder, Office web-viewer link, bare site,
|
|
48
|
+
* subsite, root site, site page, OneDrive share link) to Graph identifiers.
|
|
49
|
+
*
|
|
50
|
+
* Resolution order: SitePages URLs go to the Pages API; everything else probes /shares first
|
|
51
|
+
* (files/folders, cheap, Files.Read.All only), then falls back to site resolution on any 4xx —
|
|
52
|
+
* URL shape cannot distinguish a subsite from a folder path, so probing is the only correct
|
|
53
|
+
* strategy. Throws Error(MISSING_SITES_SCOPE_MESSAGE) when a site-level call fails with 401/403;
|
|
54
|
+
* when both probes fail, throws a combined error naming both statuses.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveItemFromUrl(authToken: string, url: string): Promise<ResolvedSharepointItem>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ApiError, axiosClient } from "../../util/axiosClient.js";
|
|
2
|
+
import { MICROSOFT_GRAPH_API_URL } from "./utils.js";
|
|
3
|
+
export const MISSING_SITES_SCOPE_MESSAGE = "Missing Sites.Read.All: this token only permits file/folder access. Site, page, and site-wide search operations require re-authentication with sites scopes.";
|
|
4
|
+
/**
|
|
5
|
+
* Encodes a URL into a Microsoft Graph share token usable with the /shares endpoint:
|
|
6
|
+
* "u!" + unpadded base64url of the URL.
|
|
7
|
+
*/
|
|
8
|
+
export function encodeShareUrl(url) {
|
|
9
|
+
const base64 = Buffer.from(url, "utf-8").toString("base64");
|
|
10
|
+
return "u!" + base64.replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
11
|
+
}
|
|
12
|
+
export function isSiteScopeError(error) {
|
|
13
|
+
return error instanceof ApiError && (error.status === 401 || error.status === 403);
|
|
14
|
+
}
|
|
15
|
+
/** GET /sites/{hostname}:{serverRelativePath} — requires Sites.Read.All. */
|
|
16
|
+
async function getSiteByPath(hostname, serverRelativePath, headers) {
|
|
17
|
+
const path = serverRelativePath.replace(/\/+$/, "");
|
|
18
|
+
// An empty path means the tenant's root site, which Graph addresses without the :path suffix
|
|
19
|
+
const requestUrl = path
|
|
20
|
+
? `${MICROSOFT_GRAPH_API_URL}/sites/${hostname}:${path}`
|
|
21
|
+
: `${MICROSOFT_GRAPH_API_URL}/sites/${hostname}`;
|
|
22
|
+
try {
|
|
23
|
+
const response = await axiosClient.get(requestUrl, { headers });
|
|
24
|
+
return response.data;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Capability is detected by attempting the call: a 401/403 here means the token lacks site scopes
|
|
28
|
+
if (isSiteScopeError(error)) {
|
|
29
|
+
throw new Error(MISSING_SITES_SCOPE_MESSAGE, { cause: error });
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Finds a site page's ID by its file name (e.g. "Home.aspx") — requires Sites.Read.All. */
|
|
35
|
+
async function findSitePageByName(siteId, pageName, headers) {
|
|
36
|
+
const pagesUrl = `${MICROSOFT_GRAPH_API_URL}/sites/${siteId}/pages/microsoft.graph.sitePage`;
|
|
37
|
+
try {
|
|
38
|
+
// Try a server-side filter first to avoid enumerating every page in the site.
|
|
39
|
+
// Caveat: the docs list $filter as supported, but Graph is known to silently
|
|
40
|
+
// ignore name filters on this endpoint, so treat the response defensively.
|
|
41
|
+
const escapedName = pageName.replace(/'/g, "''");
|
|
42
|
+
const filtered = await axiosClient.get(`${pagesUrl}?$filter=${encodeURIComponent(`name eq '${escapedName}'`)}`, { headers });
|
|
43
|
+
const match = (filtered.data.value ?? []).find(page => page.name?.toLowerCase() === pageName.toLowerCase());
|
|
44
|
+
if (match != null)
|
|
45
|
+
return match;
|
|
46
|
+
// No match and no further pages: whether the filter was honored (empty result) or
|
|
47
|
+
// ignored (we saw the complete unfiltered set), the page isn't there.
|
|
48
|
+
if (filtered.data["@odata.nextLink"] == null)
|
|
49
|
+
return undefined;
|
|
50
|
+
// No match but more pages exist — the filter was likely ignored (a known Graph gap
|
|
51
|
+
// for sitePage name filtering); fall through to full enumeration.
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isSiteScopeError(error)) {
|
|
55
|
+
throw new Error(MISSING_SITES_SCOPE_MESSAGE, { cause: error });
|
|
56
|
+
}
|
|
57
|
+
// Some tenants reject $filter on this endpoint outright — fall back to enumeration
|
|
58
|
+
if (!(error instanceof ApiError && error.status === 400)) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let nextUrl = `${pagesUrl}?$top=100`;
|
|
63
|
+
try {
|
|
64
|
+
while (nextUrl) {
|
|
65
|
+
const response = await axiosClient.get(nextUrl, { headers });
|
|
66
|
+
const pages = response.data.value ?? [];
|
|
67
|
+
const match = pages.find(page => page.name?.toLowerCase() === pageName.toLowerCase());
|
|
68
|
+
if (match != null)
|
|
69
|
+
return match;
|
|
70
|
+
nextUrl = response.data["@odata.nextLink"];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isSiteScopeError(error)) {
|
|
75
|
+
throw new Error(MISSING_SITES_SCOPE_MESSAGE, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves any pasted SharePoint/OneDrive URL (file, folder, Office web-viewer link, bare site,
|
|
83
|
+
* subsite, root site, site page, OneDrive share link) to Graph identifiers.
|
|
84
|
+
*
|
|
85
|
+
* Resolution order: SitePages URLs go to the Pages API; everything else probes /shares first
|
|
86
|
+
* (files/folders, cheap, Files.Read.All only), then falls back to site resolution on any 4xx —
|
|
87
|
+
* URL shape cannot distinguish a subsite from a folder path, so probing is the only correct
|
|
88
|
+
* strategy. Throws Error(MISSING_SITES_SCOPE_MESSAGE) when a site-level call fails with 401/403;
|
|
89
|
+
* when both probes fail, throws a combined error naming both statuses.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolveItemFromUrl(authToken, url) {
|
|
92
|
+
const headers = { Authorization: `Bearer ${authToken}` };
|
|
93
|
+
const parsed = new URL(url);
|
|
94
|
+
// Site pages: https://{tenant}.sharepoint.com/sites/{site}/SitePages/{page}.aspx
|
|
95
|
+
const sitePageMatch = parsed.pathname.match(/^(.*)\/SitePages\/([^/]+\.aspx)$/i);
|
|
96
|
+
if (sitePageMatch) {
|
|
97
|
+
const [, sitePath, pageFileName] = sitePageMatch;
|
|
98
|
+
const pageName = decodeURIComponent(pageFileName);
|
|
99
|
+
const site = await getSiteByPath(parsed.hostname, sitePath, headers);
|
|
100
|
+
const page = await findSitePageByName(site.id, pageName, headers);
|
|
101
|
+
return {
|
|
102
|
+
itemType: "page",
|
|
103
|
+
siteId: site.id,
|
|
104
|
+
pageId: page?.id,
|
|
105
|
+
name: page?.title ?? page?.name ?? pageName,
|
|
106
|
+
webUrl: page?.webUrl ?? url,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// The /shares resolver handles files, folders, Doc.aspx web-viewer links, OneDrive personal
|
|
110
|
+
// and 1drv.ms share links with Files.Read.All alone
|
|
111
|
+
let shareError;
|
|
112
|
+
try {
|
|
113
|
+
const response = await axiosClient.get(`${MICROSOFT_GRAPH_API_URL}/shares/${encodeShareUrl(url)}/driveItem`, {
|
|
114
|
+
headers,
|
|
115
|
+
});
|
|
116
|
+
const item = response.data;
|
|
117
|
+
if (item?.id && item?.parentReference?.driveId) {
|
|
118
|
+
return {
|
|
119
|
+
itemType: item.folder ? "folder" : "file",
|
|
120
|
+
driveId: item.parentReference.driveId,
|
|
121
|
+
itemId: item.id,
|
|
122
|
+
siteId: item.parentReference.siteId,
|
|
123
|
+
name: item.name,
|
|
124
|
+
webUrl: item.webUrl,
|
|
125
|
+
mimeType: item.file?.mimeType,
|
|
126
|
+
sizeBytes: item.size,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Unable to resolve URL to a drive item: ${url}`);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// A site is not a drive item, so /shares rejects site URLs with a 4xx. URL shape cannot
|
|
133
|
+
// distinguish a subsite from a folder path, so we always probe /shares first and, on any
|
|
134
|
+
// client error, fall back to resolving the path as a site. 5xx/network errors are real
|
|
135
|
+
// failures and are rethrown immediately.
|
|
136
|
+
if (!(error instanceof ApiError && error.status !== undefined && error.status < 500)) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
shareError = error;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const site = await getSiteByPath(parsed.hostname, parsed.pathname, headers);
|
|
143
|
+
return { itemType: "site", siteId: site.id, name: site.displayName, webUrl: site.webUrl };
|
|
144
|
+
}
|
|
145
|
+
catch (siteError) {
|
|
146
|
+
// A missing sites scope is the more actionable diagnosis — surface it as-is
|
|
147
|
+
if (siteError instanceof Error && siteError.message === MISSING_SITES_SCOPE_MESSAGE) {
|
|
148
|
+
throw siteError;
|
|
149
|
+
}
|
|
150
|
+
const siteStatus = siteError instanceof ApiError && siteError.status !== undefined ? siteError.status : "error";
|
|
151
|
+
throw new Error(`Unable to resolve URL: not a document or folder (/shares: ${shareError?.status ?? "error"}) and not a site (/sites: ${siteStatus}): ${url}`, { cause: siteError });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -7,3 +7,4 @@ export declare function getGraphClient(authParams: AuthParamsType): Promise<Clie
|
|
|
7
7
|
* @returns A sanitized filename that is safe to use.
|
|
8
8
|
*/
|
|
9
9
|
export declare function validateAndSanitizeFileName(fileName: string): string;
|
|
10
|
+
export declare const MICROSOFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { computeTimeToClosureMinutes, extractAdditionalFields, queryServiceNowTable } from "./utils/tableQuery.js";
|
|
2
|
+
const CHANGE_REQUEST_FIELDS = [
|
|
3
|
+
"number",
|
|
4
|
+
"short_description",
|
|
5
|
+
"description",
|
|
6
|
+
"state",
|
|
7
|
+
"priority",
|
|
8
|
+
"impact",
|
|
9
|
+
"urgency",
|
|
10
|
+
"requested_by",
|
|
11
|
+
"assigned_to",
|
|
12
|
+
"assignment_group",
|
|
13
|
+
"opened_at",
|
|
14
|
+
"sys_updated_on",
|
|
15
|
+
"closed_at",
|
|
16
|
+
"work_notes",
|
|
17
|
+
"comments",
|
|
18
|
+
];
|
|
19
|
+
const getChangeRequests = async ({ params, authParams, }) => {
|
|
20
|
+
const { query, additionalFields, limit, offset } = params;
|
|
21
|
+
const result = await queryServiceNowTable({
|
|
22
|
+
authParams,
|
|
23
|
+
tableName: "change_request",
|
|
24
|
+
fields: CHANGE_REQUEST_FIELDS,
|
|
25
|
+
filter: query,
|
|
26
|
+
additionalFields,
|
|
27
|
+
limit,
|
|
28
|
+
offset,
|
|
29
|
+
});
|
|
30
|
+
if ("error" in result) {
|
|
31
|
+
return { success: false, error: result.error };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
records: result.records.map(record => ({
|
|
36
|
+
number: record.number,
|
|
37
|
+
requestor: record.requested_by,
|
|
38
|
+
assignee: record.assigned_to,
|
|
39
|
+
assignmentGroup: record.assignment_group,
|
|
40
|
+
shortDescription: record.short_description,
|
|
41
|
+
description: record.description,
|
|
42
|
+
state: record.state,
|
|
43
|
+
priority: record.priority,
|
|
44
|
+
impact: record.impact,
|
|
45
|
+
urgency: record.urgency,
|
|
46
|
+
openedAt: record.opened_at,
|
|
47
|
+
updatedAt: record.sys_updated_on,
|
|
48
|
+
closedAt: record.closed_at,
|
|
49
|
+
workNotes: record.work_notes,
|
|
50
|
+
comments: record.comments,
|
|
51
|
+
timeToClosureMinutes: computeTimeToClosureMinutes(record.opened_at, record.closed_at),
|
|
52
|
+
extraFields: extractAdditionalFields(record, additionalFields),
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export default getChangeRequests;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { computeTimeToClosureMinutes, extractAdditionalFields, queryServiceNowTable } from "./utils/tableQuery.js";
|
|
2
|
+
const INCIDENT_FIELDS = [
|
|
3
|
+
"number",
|
|
4
|
+
"short_description",
|
|
5
|
+
"description",
|
|
6
|
+
"state",
|
|
7
|
+
"incident_state",
|
|
8
|
+
"priority",
|
|
9
|
+
"impact",
|
|
10
|
+
"urgency",
|
|
11
|
+
"caller_id",
|
|
12
|
+
"assigned_to",
|
|
13
|
+
"assignment_group",
|
|
14
|
+
"opened_at",
|
|
15
|
+
"sys_updated_on",
|
|
16
|
+
"closed_at",
|
|
17
|
+
"work_notes",
|
|
18
|
+
"comments",
|
|
19
|
+
];
|
|
20
|
+
const getIncidents = async ({ params, authParams, }) => {
|
|
21
|
+
const { query, additionalFields, limit, offset } = params;
|
|
22
|
+
const result = await queryServiceNowTable({
|
|
23
|
+
authParams,
|
|
24
|
+
tableName: "incident",
|
|
25
|
+
fields: INCIDENT_FIELDS,
|
|
26
|
+
filter: query,
|
|
27
|
+
additionalFields,
|
|
28
|
+
limit,
|
|
29
|
+
offset,
|
|
30
|
+
});
|
|
31
|
+
if ("error" in result) {
|
|
32
|
+
return { success: false, error: result.error };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
records: result.records.map(record => ({
|
|
37
|
+
number: record.number,
|
|
38
|
+
caller: record.caller_id,
|
|
39
|
+
assignee: record.assigned_to,
|
|
40
|
+
assignmentGroup: record.assignment_group,
|
|
41
|
+
shortDescription: record.short_description,
|
|
42
|
+
description: record.description,
|
|
43
|
+
state: record.state,
|
|
44
|
+
incidentState: record.incident_state,
|
|
45
|
+
priority: record.priority,
|
|
46
|
+
impact: record.impact,
|
|
47
|
+
urgency: record.urgency,
|
|
48
|
+
openedAt: record.opened_at,
|
|
49
|
+
updatedAt: record.sys_updated_on,
|
|
50
|
+
closedAt: record.closed_at,
|
|
51
|
+
workNotes: record.work_notes,
|
|
52
|
+
comments: record.comments,
|
|
53
|
+
timeToClosureMinutes: computeTimeToClosureMinutes(record.opened_at, record.closed_at),
|
|
54
|
+
extraFields: extractAdditionalFields(record, additionalFields),
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export default getIncidents;
|