@red-hat-developer-hub/backstage-plugin-intelligent-assistant 5.2.0 → 5.3.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/CHANGELOG.md +21 -0
- package/README.md +14 -2
- package/dist/alpha.d.ts +24 -0
- package/dist/components/AttachmentContext.esm.js +1 -1
- package/dist/components/ChatLoadingFallback.esm.js +37 -0
- package/dist/components/ChatLoadingFallback.esm.js.map +1 -0
- package/dist/components/CollapsedHistoryStrip.esm.js +1 -1
- package/dist/components/LightSpeedChat.esm.js +76 -29
- package/dist/components/LightSpeedChat.esm.js.map +1 -1
- package/dist/components/LightspeedChatBox.esm.js +2 -2
- package/dist/components/LightspeedChatBoxHeader.esm.js +30 -2
- package/dist/components/LightspeedChatBoxHeader.esm.js.map +1 -1
- package/dist/components/LightspeedChatContainer.esm.js +20 -4
- package/dist/components/LightspeedChatContainer.esm.js.map +1 -1
- package/dist/components/LightspeedDrawerContext.esm.js.map +1 -1
- package/dist/components/LightspeedDrawerProvider.esm.js +11 -25
- package/dist/components/LightspeedDrawerProvider.esm.js.map +1 -1
- package/dist/components/LightspeedFABContent.esm.js +14 -2
- package/dist/components/LightspeedFABContent.esm.js.map +1 -1
- package/dist/components/LightspeedOverlayChat.esm.js +39 -0
- package/dist/components/LightspeedOverlayChat.esm.js.map +1 -0
- package/dist/components/McpConfigureServerModal.esm.js +2 -2
- package/dist/components/MessageBarModelSelector.esm.js +105 -13
- package/dist/components/MessageBarModelSelector.esm.js.map +1 -1
- package/dist/components/ScreenContextChip.esm.js +172 -0
- package/dist/components/ScreenContextChip.esm.js.map +1 -0
- package/dist/components/ToastAlertGroup.esm.js +1 -1
- package/dist/components/criticalFabBundle.esm.js +3 -0
- package/dist/components/criticalFabBundle.esm.js.map +1 -0
- package/dist/components/loaders.esm.js +11 -0
- package/dist/components/loaders.esm.js.map +1 -0
- package/dist/components/notebooks/AddDocumentModal.esm.js +2 -2
- package/dist/components/notebooks/DocumentSidebar.esm.js +1 -1
- package/dist/components/notebooks/NotebookCard.esm.js +1 -1
- package/dist/components/notebooks/NotebookStreamProvider.esm.js +1 -1
- package/dist/components/notebooks/NotebookView.esm.js +3 -3
- package/dist/components/notebooks/OverwriteConfirmModal.esm.js +1 -1
- package/dist/components/notebooks/UploadResourceScreen.esm.js +1 -1
- package/dist/components/notebooks/notebookStreamStore.esm.js +1 -1
- package/dist/components/notebooks/notebookStreamStore.esm.js.map +1 -1
- package/dist/const.esm.js +9 -1
- package/dist/const.esm.js.map +1 -1
- package/dist/fabModule.esm.js +51 -0
- package/dist/fabModule.esm.js.map +1 -0
- package/dist/hooks/screenContextLabelSubscription.esm.js +113 -0
- package/dist/hooks/screenContextLabelSubscription.esm.js.map +1 -0
- package/dist/hooks/useConversationMessages.esm.js +2 -1
- package/dist/hooks/useConversationMessages.esm.js.map +1 -1
- package/dist/hooks/useDisplayModeSettings.esm.js +1 -1
- package/dist/hooks/useDisplayModeSettings.esm.js.map +1 -1
- package/dist/hooks/useLightspeedProviderState.esm.js +1 -2
- package/dist/hooks/useLightspeedProviderState.esm.js.map +1 -1
- package/dist/hooks/useScreenContextLabelRevision.esm.js +13 -0
- package/dist/hooks/useScreenContextLabelRevision.esm.js.map +1 -0
- package/dist/hooks/useScreenContextSettings.esm.js +110 -0
- package/dist/hooks/useScreenContextSettings.esm.js.map +1 -0
- package/dist/hooks/useWindowLocation.esm.js +28 -0
- package/dist/hooks/useWindowLocation.esm.js.map +1 -0
- package/dist/index.d.ts +1 -15
- package/dist/index.esm.js +7 -69
- package/dist/index.esm.js.map +1 -1
- package/dist/intelligent-assistant-fab-module.d.ts +12 -4
- package/dist/intelligent-assistant-fab-module.esm.js +1 -1
- package/dist/intelligent-assistant-translations-module.d.ts +10 -4
- package/dist/intelligent-assistant-translations-module.esm.js +1 -1
- package/dist/lazy/LazyLightspeedChatDrawerContent.esm.js +13 -0
- package/dist/lazy/LazyLightspeedChatDrawerContent.esm.js.map +1 -0
- package/dist/lazy/LazyLightspeedFabRootWrapper.esm.js +17 -0
- package/dist/lazy/LazyLightspeedFabRootWrapper.esm.js.map +1 -0
- package/dist/legacy.d.ts +14 -16
- package/dist/legacy.esm.js +2 -9
- package/dist/legacy.esm.js.map +1 -1
- package/dist/loadChatPatternflyStyles.esm.js +14 -0
- package/dist/loadChatPatternflyStyles.esm.js.map +1 -0
- package/dist/muiClassNameConfig.esm.js +6 -0
- package/dist/muiClassNameConfig.esm.js.map +1 -0
- package/dist/plugin.esm.js +4 -4
- package/dist/plugin.esm.js.map +1 -1
- package/dist/translations/de.esm.js +24 -0
- package/dist/translations/de.esm.js.map +1 -1
- package/dist/translations/es.esm.js +24 -0
- package/dist/translations/es.esm.js.map +1 -1
- package/dist/translations/fr.esm.js +24 -0
- package/dist/translations/fr.esm.js.map +1 -1
- package/dist/translations/it.esm.js +24 -0
- package/dist/translations/it.esm.js.map +1 -1
- package/dist/translations/ja.esm.js +24 -0
- package/dist/translations/ja.esm.js.map +1 -1
- package/dist/translations/ref.esm.js +24 -0
- package/dist/translations/ref.esm.js.map +1 -1
- package/dist/translationsModule.esm.js +18 -0
- package/dist/translationsModule.esm.js.map +1 -0
- package/dist/types.esm.js.map +1 -1
- package/dist/utils/dom-selectors.esm.js +22 -2
- package/dist/utils/dom-selectors.esm.js.map +1 -1
- package/dist/utils/lightspeed-chatbox-message-utils.esm.js +145 -0
- package/dist/utils/lightspeed-chatbox-message-utils.esm.js.map +1 -0
- package/dist/utils/lightspeed-chatbox-utils.esm.js +20 -144
- package/dist/utils/lightspeed-chatbox-utils.esm.js.map +1 -1
- package/dist/utils/screen-capture.esm.js +116 -0
- package/dist/utils/screen-capture.esm.js.map +1 -0
- package/dist/utils/screen-context-utils.esm.js +343 -0
- package/dist/utils/screen-context-utils.esm.js.map +1 -0
- package/dist/utils/sensitive-data-redactor.esm.js +39 -1
- package/dist/utils/sensitive-data-redactor.esm.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { extractPageContext } from './dom-extractor.esm.js';
|
|
2
|
+
import { SEARCH_INPUT_SELECTOR, MODAL_DIALOG_SELECTOR, DIALOG_TITLE_SELECTOR, PAGE_CHROME_ROOT_SELECTORS, PAGE_TITLE_SCOPED_SELECTORS, PLUGIN_HEADER_TOOLBAR_NAME_SELECTOR, PLUGIN_HEADER_BREADCRUMBS_SELECTOR, LEGACY_PAGE_TITLE_SELECTORS, TEMPLATE_INFO_CARD_TITLE_SELECTORS } from './dom-selectors.esm.js';
|
|
3
|
+
import { captureScreenshot } from './screen-capture.esm.js';
|
|
4
|
+
|
|
5
|
+
const SCREEN_CONTEXT_CHIP_MAX_LABEL_LENGTH = 40;
|
|
6
|
+
const SCREEN_CONTEXT_EXCLUDE_SELECTOR = "[data-screen-capture-exclude], .pf-chatbot";
|
|
7
|
+
const SEARCH_QUERY_PARAM_KEYS = ["query", "term", "q"];
|
|
8
|
+
function truncateChipLabel(label, maxLength = SCREEN_CONTEXT_CHIP_MAX_LABEL_LENGTH) {
|
|
9
|
+
const trimmed = label.trim();
|
|
10
|
+
if (trimmed.length <= maxLength) {
|
|
11
|
+
return trimmed;
|
|
12
|
+
}
|
|
13
|
+
return `${trimmed.slice(0, maxLength - 1).trimEnd()}\u2026`;
|
|
14
|
+
}
|
|
15
|
+
function normalizeDocumentTitle(title) {
|
|
16
|
+
const trimmed = title.trim();
|
|
17
|
+
const pipeIndex = trimmed.indexOf(" | ");
|
|
18
|
+
if (pipeIndex > 0) {
|
|
19
|
+
return trimmed.slice(0, pipeIndex).trim();
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
function isElementVisible(el) {
|
|
24
|
+
if (!(el instanceof HTMLElement)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (el.closest(SCREEN_CONTEXT_EXCLUDE_SELECTOR)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const style = window.getComputedStyle(el);
|
|
31
|
+
if (el.hasAttribute("hidden")) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (style.display === "none" || style.visibility === "hidden" || Number.parseFloat(style.opacity) === 0) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function queryFirstVisibleTitle(root, selectors) {
|
|
40
|
+
for (const selector of selectors) {
|
|
41
|
+
const elements = root.querySelectorAll(selector);
|
|
42
|
+
for (const el of elements) {
|
|
43
|
+
if (!isElementVisible(el)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const text = el.textContent?.trim();
|
|
47
|
+
if (text) {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
function getChromeRoots(doc) {
|
|
55
|
+
const roots = [];
|
|
56
|
+
for (const selector of PAGE_CHROME_ROOT_SELECTORS) {
|
|
57
|
+
const node = doc.querySelector(selector);
|
|
58
|
+
if (node && !roots.includes(node)) {
|
|
59
|
+
roots.push(node);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return roots.length > 0 ? roots : [doc];
|
|
63
|
+
}
|
|
64
|
+
function getPluginHeaderToolbarChipLabel(root = document) {
|
|
65
|
+
const toolbarName = root.querySelector(PLUGIN_HEADER_TOOLBAR_NAME_SELECTOR);
|
|
66
|
+
if (!(toolbarName instanceof Element)) {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
if (toolbarName.closest(SCREEN_CONTEXT_EXCLUDE_SELECTOR)) {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
const breadcrumbs = toolbarName.querySelector(
|
|
73
|
+
PLUGIN_HEADER_BREADCRUMBS_SELECTOR
|
|
74
|
+
);
|
|
75
|
+
if (!breadcrumbs) {
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
const breadcrumbItems = breadcrumbs.querySelectorAll(
|
|
79
|
+
'li.react-aria-Breadcrumb, li[class*="Breadcrumb"]'
|
|
80
|
+
);
|
|
81
|
+
const hasNestedCrumbs = breadcrumbItems.length >= 2;
|
|
82
|
+
const h1Text = toolbarName.querySelector("h1")?.textContent?.trim();
|
|
83
|
+
if (h1Text) {
|
|
84
|
+
return h1Text;
|
|
85
|
+
}
|
|
86
|
+
if (hasNestedCrumbs) {
|
|
87
|
+
const firstLink = breadcrumbs.querySelector("a[href]");
|
|
88
|
+
const linkText = firstLink?.textContent?.trim();
|
|
89
|
+
if (linkText) {
|
|
90
|
+
return linkText;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const currentCrumb = breadcrumbs.querySelector(
|
|
94
|
+
'[data-current="true"], [aria-current="page"]'
|
|
95
|
+
);
|
|
96
|
+
const currentText = currentCrumb?.textContent?.trim();
|
|
97
|
+
if (currentText) {
|
|
98
|
+
return currentText;
|
|
99
|
+
}
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
function getPageTitleFromDom(doc = document) {
|
|
103
|
+
const dialog = doc.querySelector(MODAL_DIALOG_SELECTOR);
|
|
104
|
+
if (dialog) {
|
|
105
|
+
const dialogTitle = dialog.querySelector(DIALOG_TITLE_SELECTOR);
|
|
106
|
+
if (dialogTitle && isElementVisible(dialogTitle)) {
|
|
107
|
+
const text = dialogTitle.textContent?.trim();
|
|
108
|
+
if (text) {
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const roots = getChromeRoots(doc);
|
|
114
|
+
for (const root of roots) {
|
|
115
|
+
const buiTitle = queryFirstVisibleTitle(root, PAGE_TITLE_SCOPED_SELECTORS);
|
|
116
|
+
if (buiTitle) {
|
|
117
|
+
return buiTitle;
|
|
118
|
+
}
|
|
119
|
+
const pluginToolbarLabel = getPluginHeaderToolbarChipLabel(root);
|
|
120
|
+
if (pluginToolbarLabel) {
|
|
121
|
+
return pluginToolbarLabel;
|
|
122
|
+
}
|
|
123
|
+
const legacyTitle = queryFirstVisibleTitle(
|
|
124
|
+
root,
|
|
125
|
+
LEGACY_PAGE_TITLE_SELECTORS
|
|
126
|
+
);
|
|
127
|
+
if (legacyTitle) {
|
|
128
|
+
return legacyTitle;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const normalized = normalizeDocumentTitle(doc.title);
|
|
132
|
+
return normalized || "Current page";
|
|
133
|
+
}
|
|
134
|
+
function getScreenContextRouteKind(pathname, search) {
|
|
135
|
+
if (pathname.includes("/create/") || pathname.includes("/template/") || /\/template\/[^/]+/.test(pathname) || pathname.includes("/edit/") && pathname.includes("template")) {
|
|
136
|
+
return "template";
|
|
137
|
+
}
|
|
138
|
+
if (pathname.includes("/search") || SEARCH_QUERY_PARAM_KEYS.some((key) => new URLSearchParams(search).has(key)) || search.includes("filters[") || pathname.includes("/catalog") && /[?&](query|term|filters\[)/.test(search)) {
|
|
139
|
+
return "search";
|
|
140
|
+
}
|
|
141
|
+
return "default";
|
|
142
|
+
}
|
|
143
|
+
function getSearchQueryFromLocation(search, doc = document) {
|
|
144
|
+
const params = new URLSearchParams(
|
|
145
|
+
search.startsWith("?") ? search : `?${search}`
|
|
146
|
+
);
|
|
147
|
+
for (const key of SEARCH_QUERY_PARAM_KEYS) {
|
|
148
|
+
const fromUrl = params.get(key)?.trim();
|
|
149
|
+
if (fromUrl) {
|
|
150
|
+
return fromUrl;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const inputs = doc.querySelectorAll(SEARCH_INPUT_SELECTOR);
|
|
154
|
+
for (const input of inputs) {
|
|
155
|
+
if (!(input instanceof HTMLInputElement)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!isElementVisible(input)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const fromInput = input.value?.trim();
|
|
162
|
+
if (fromInput) {
|
|
163
|
+
return fromInput;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
const SOFTWARE_TEMPLATES_LIST_CHIP_LABEL = "Software templates";
|
|
169
|
+
const SOFTWARE_TEMPLATES_GALLERY_PATH = "/create/templates";
|
|
170
|
+
function normalizePathname(pathname) {
|
|
171
|
+
const trimmed = pathname.trim();
|
|
172
|
+
if (!trimmed || trimmed === "/") {
|
|
173
|
+
return "/";
|
|
174
|
+
}
|
|
175
|
+
return trimmed.replace(/\/+$/, "") || "/";
|
|
176
|
+
}
|
|
177
|
+
function isSoftwareTemplatesListPath(pathname) {
|
|
178
|
+
return normalizePathname(pathname) === SOFTWARE_TEMPLATES_GALLERY_PATH;
|
|
179
|
+
}
|
|
180
|
+
function parseSoftwareTemplateDetailSlug(pathname) {
|
|
181
|
+
const normalized = normalizePathname(pathname);
|
|
182
|
+
const prefix = `${SOFTWARE_TEMPLATES_GALLERY_PATH}/`;
|
|
183
|
+
if (!normalized.startsWith(prefix)) {
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
const segments = normalized.slice(prefix.length).split("/").filter(Boolean);
|
|
187
|
+
if (segments.length < 2) {
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
return segments[segments.length - 1];
|
|
191
|
+
}
|
|
192
|
+
function getSoftwareTemplateTitleFromDom(doc = document) {
|
|
193
|
+
const roots = getChromeRoots(doc);
|
|
194
|
+
for (const root of roots) {
|
|
195
|
+
const title = queryFirstVisibleTitle(
|
|
196
|
+
root,
|
|
197
|
+
TEMPLATE_INFO_CARD_TITLE_SELECTORS
|
|
198
|
+
);
|
|
199
|
+
if (title) {
|
|
200
|
+
return title;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
function resolveSoftwareTemplateChipLabel(pathname, doc = document) {
|
|
206
|
+
if (isSoftwareTemplatesListPath(pathname)) {
|
|
207
|
+
return SOFTWARE_TEMPLATES_LIST_CHIP_LABEL;
|
|
208
|
+
}
|
|
209
|
+
const slug = parseSoftwareTemplateDetailSlug(pathname);
|
|
210
|
+
if (!slug) {
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
return getSoftwareTemplateTitleFromDom(doc) ?? slug;
|
|
214
|
+
}
|
|
215
|
+
function resolveScreenContextChipLabel(options) {
|
|
216
|
+
const doc = options.document ?? document;
|
|
217
|
+
const routeKind = getScreenContextRouteKind(options.pathname, options.search);
|
|
218
|
+
if (routeKind === "search") {
|
|
219
|
+
const searchQuery = getSearchQueryFromLocation(options.search, doc);
|
|
220
|
+
if (searchQuery) {
|
|
221
|
+
return {
|
|
222
|
+
label: truncateChipLabel(searchQuery),
|
|
223
|
+
routeKind,
|
|
224
|
+
searchQuery
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (routeKind === "template") {
|
|
229
|
+
const softwareTemplateLabel = resolveSoftwareTemplateChipLabel(
|
|
230
|
+
options.pathname,
|
|
231
|
+
doc
|
|
232
|
+
);
|
|
233
|
+
if (softwareTemplateLabel) {
|
|
234
|
+
return {
|
|
235
|
+
label: truncateChipLabel(softwareTemplateLabel),
|
|
236
|
+
routeKind
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const pageTitle = getPageTitleFromDom(doc);
|
|
241
|
+
return {
|
|
242
|
+
label: truncateChipLabel(pageTitle),
|
|
243
|
+
routeKind,
|
|
244
|
+
searchQuery: routeKind === "search" ? getSearchQueryFromLocation(options.search, doc) : void 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function getScreenContextTooltipLine1Key(routeKind, pathname) {
|
|
248
|
+
switch (routeKind) {
|
|
249
|
+
case "template":
|
|
250
|
+
if (pathname && isSoftwareTemplatesListPath(pathname)) {
|
|
251
|
+
return "contextChip.tooltip.askAbout";
|
|
252
|
+
}
|
|
253
|
+
return "contextChip.tooltip.template";
|
|
254
|
+
case "search":
|
|
255
|
+
return "contextChip.tooltip.search";
|
|
256
|
+
default:
|
|
257
|
+
return "contextChip.tooltip.askAbout";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function getScreenContextTooltipLine1Text(params, t) {
|
|
261
|
+
const label = params.chipLabel;
|
|
262
|
+
const key = getScreenContextTooltipLine1Key(
|
|
263
|
+
params.routeKind,
|
|
264
|
+
params.pathname
|
|
265
|
+
);
|
|
266
|
+
return t(key, { label });
|
|
267
|
+
}
|
|
268
|
+
function getScreenContextTooltipLine2Key(input) {
|
|
269
|
+
const { domEnabled, screenshotsEnabled, supportsVision } = input;
|
|
270
|
+
const willSendDom = domEnabled;
|
|
271
|
+
const willSendScreenshot = screenshotsEnabled && supportsVision;
|
|
272
|
+
if (willSendDom && willSendScreenshot) {
|
|
273
|
+
return "fullContext";
|
|
274
|
+
}
|
|
275
|
+
if (!domEnabled && !screenshotsEnabled) {
|
|
276
|
+
return "adminLimited";
|
|
277
|
+
}
|
|
278
|
+
if (!domEnabled && screenshotsEnabled && supportsVision) {
|
|
279
|
+
return "screenshotOnly";
|
|
280
|
+
}
|
|
281
|
+
if (willSendDom) {
|
|
282
|
+
const adminOff = !screenshotsEnabled;
|
|
283
|
+
const noVision = !supportsVision;
|
|
284
|
+
if (adminOff && noVision) {
|
|
285
|
+
return "textOnlyCombined";
|
|
286
|
+
}
|
|
287
|
+
if (noVision) {
|
|
288
|
+
return "textOnlyNoVision";
|
|
289
|
+
}
|
|
290
|
+
if (adminOff) {
|
|
291
|
+
return "textOnlyAdminScreenshotsOff";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!domEnabled && screenshotsEnabled && !supportsVision) {
|
|
295
|
+
return "domOffNoVision";
|
|
296
|
+
}
|
|
297
|
+
return "adminLimited";
|
|
298
|
+
}
|
|
299
|
+
function shouldAttachScreenContext(input) {
|
|
300
|
+
const baseGate = input.adminEnabled && input.sharingEnabled && !input.paused && !input.isFullscreen;
|
|
301
|
+
if (!baseGate) {
|
|
302
|
+
return { attachDom: false, attachScreenshot: false };
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
attachDom: input.domEnabled,
|
|
306
|
+
attachScreenshot: input.screenshotsEnabled && input.supportsVision
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function buildScreenContextAttachments(input) {
|
|
310
|
+
const { attachDom, attachScreenshot } = shouldAttachScreenContext(input);
|
|
311
|
+
const attachments = [];
|
|
312
|
+
if (attachDom) {
|
|
313
|
+
try {
|
|
314
|
+
const domContext = extractPageContext({
|
|
315
|
+
maxChars: input.domExtractionMaxChars
|
|
316
|
+
});
|
|
317
|
+
if (domContext) {
|
|
318
|
+
attachments.push({
|
|
319
|
+
attachment_type: "configuration",
|
|
320
|
+
content_type: "text/plain",
|
|
321
|
+
content: domContext
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (attachScreenshot) {
|
|
328
|
+
const result = await captureScreenshot({
|
|
329
|
+
excludeSelector: "[data-screen-capture-exclude]"
|
|
330
|
+
});
|
|
331
|
+
if (result.success) {
|
|
332
|
+
attachments.push({
|
|
333
|
+
attachment_type: "image",
|
|
334
|
+
content_type: result.contentType,
|
|
335
|
+
content: result.base64
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return attachments;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export { SCREEN_CONTEXT_CHIP_MAX_LABEL_LENGTH, SOFTWARE_TEMPLATES_LIST_CHIP_LABEL, buildScreenContextAttachments, getPageTitleFromDom, getPluginHeaderToolbarChipLabel, getScreenContextRouteKind, getScreenContextTooltipLine1Key, getScreenContextTooltipLine1Text, getScreenContextTooltipLine2Key, getSearchQueryFromLocation, getSoftwareTemplateTitleFromDom, isElementVisible, isSoftwareTemplatesListPath, normalizeDocumentTitle, parseSoftwareTemplateDetailSlug, resolveScreenContextChipLabel, resolveSoftwareTemplateChipLabel, shouldAttachScreenContext, truncateChipLabel };
|
|
343
|
+
//# sourceMappingURL=screen-context-utils.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen-context-utils.esm.js","sources":["../../src/utils/screen-context-utils.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Attachment } from '../types';\nimport { extractPageContext } from './dom-extractor';\nimport {\n DIALOG_TITLE_SELECTOR,\n LEGACY_PAGE_TITLE_SELECTORS,\n MODAL_DIALOG_SELECTOR,\n PAGE_CHROME_ROOT_SELECTORS,\n PAGE_TITLE_SCOPED_SELECTORS,\n PLUGIN_HEADER_BREADCRUMBS_SELECTOR,\n PLUGIN_HEADER_TOOLBAR_NAME_SELECTOR,\n SEARCH_INPUT_SELECTOR,\n TEMPLATE_INFO_CARD_TITLE_SELECTORS,\n} from './dom-selectors';\nimport { captureScreenshot } from './screen-capture';\n\nexport const SCREEN_CONTEXT_CHIP_MAX_LABEL_LENGTH = 40;\n\nconst SCREEN_CONTEXT_EXCLUDE_SELECTOR =\n '[data-screen-capture-exclude], .pf-chatbot';\n\nconst SEARCH_QUERY_PARAM_KEYS = ['query', 'term', 'q'] as const;\n\nexport type ScreenContextRouteKind = 'default' | 'template' | 'search';\n\nexport type ScreenContextTooltipLine2Key =\n | 'fullContext'\n | 'adminLimited'\n | 'screenshotOnly'\n | 'domOffNoVision'\n | 'textOnlyNoVision'\n | 'textOnlyAdminScreenshotsOff'\n | 'textOnlyCombined';\n\nexport type ShouldAttachScreenContextInput = {\n adminEnabled: boolean;\n sharingEnabled: boolean;\n paused: boolean;\n isFullscreen: boolean;\n domEnabled: boolean;\n screenshotsEnabled: boolean;\n supportsVision: boolean;\n};\n\nexport type ShouldAttachScreenContextResult = {\n attachDom: boolean;\n attachScreenshot: boolean;\n};\n\nexport type ResolvedScreenContextLabel = {\n label: string;\n routeKind: ScreenContextRouteKind;\n searchQuery?: string;\n};\n\nexport function truncateChipLabel(\n label: string,\n maxLength = SCREEN_CONTEXT_CHIP_MAX_LABEL_LENGTH,\n): string {\n const trimmed = label.trim();\n if (trimmed.length <= maxLength) {\n return trimmed;\n }\n return `${trimmed.slice(0, maxLength - 1).trimEnd()}…`;\n}\n\nexport function normalizeDocumentTitle(title: string): string {\n const trimmed = title.trim();\n const pipeIndex = trimmed.indexOf(' | ');\n if (pipeIndex > 0) {\n return trimmed.slice(0, pipeIndex).trim();\n }\n return trimmed;\n}\n\nexport function isElementVisible(el: Element): boolean {\n if (!(el instanceof HTMLElement)) {\n return false;\n }\n if (el.closest(SCREEN_CONTEXT_EXCLUDE_SELECTOR)) {\n return false;\n }\n const style = window.getComputedStyle(el);\n if (el.hasAttribute('hidden')) {\n return false;\n }\n if (\n style.display === 'none' ||\n style.visibility === 'hidden' ||\n Number.parseFloat(style.opacity) === 0\n ) {\n return false;\n }\n return true;\n}\n\nfunction queryFirstVisibleTitle(\n root: ParentNode,\n selectors: readonly string[],\n): string | undefined {\n for (const selector of selectors) {\n const elements = root.querySelectorAll(selector);\n for (const el of elements) {\n if (!isElementVisible(el)) {\n continue;\n }\n const text = el.textContent?.trim();\n if (text) {\n return text;\n }\n }\n }\n return undefined;\n}\n\nfunction getChromeRoots(doc: Document): ParentNode[] {\n const roots: ParentNode[] = [];\n for (const selector of PAGE_CHROME_ROOT_SELECTORS) {\n const node = doc.querySelector(selector);\n if (node && !roots.includes(node)) {\n roots.push(node);\n }\n }\n return roots.length > 0 ? roots : [doc];\n}\n\n/**\n * BUI plugin pages (e.g. Settings → General) use toolbar breadcrumbs instead of\n * HeaderTitle. Prefer the plugin name (visually hidden h1 or first crumb link).\n */\nexport function getPluginHeaderToolbarChipLabel(\n root: ParentNode = document,\n): string | undefined {\n const toolbarName = root.querySelector(PLUGIN_HEADER_TOOLBAR_NAME_SELECTOR);\n if (!(toolbarName instanceof Element)) {\n return undefined;\n }\n if (toolbarName.closest(SCREEN_CONTEXT_EXCLUDE_SELECTOR)) {\n return undefined;\n }\n\n const breadcrumbs = toolbarName.querySelector(\n PLUGIN_HEADER_BREADCRUMBS_SELECTOR,\n );\n if (!breadcrumbs) {\n return undefined;\n }\n\n const breadcrumbItems = breadcrumbs.querySelectorAll(\n 'li.react-aria-Breadcrumb, li[class*=\"Breadcrumb\"]',\n );\n const hasNestedCrumbs = breadcrumbItems.length >= 2;\n\n const h1Text = toolbarName.querySelector('h1')?.textContent?.trim();\n if (h1Text) {\n return h1Text;\n }\n\n if (hasNestedCrumbs) {\n const firstLink = breadcrumbs.querySelector('a[href]');\n const linkText = firstLink?.textContent?.trim();\n if (linkText) {\n return linkText;\n }\n }\n\n const currentCrumb = breadcrumbs.querySelector(\n '[data-current=\"true\"], [aria-current=\"page\"]',\n );\n const currentText = currentCrumb?.textContent?.trim();\n if (currentText) {\n return currentText;\n }\n\n return undefined;\n}\n\nexport function getPageTitleFromDom(doc: Document = document): string {\n const dialog = doc.querySelector(MODAL_DIALOG_SELECTOR);\n if (dialog) {\n const dialogTitle = dialog.querySelector(DIALOG_TITLE_SELECTOR);\n if (dialogTitle && isElementVisible(dialogTitle)) {\n const text = dialogTitle.textContent?.trim();\n if (text) {\n return text;\n }\n }\n }\n\n const roots = getChromeRoots(doc);\n for (const root of roots) {\n const buiTitle = queryFirstVisibleTitle(root, PAGE_TITLE_SCOPED_SELECTORS);\n if (buiTitle) {\n return buiTitle;\n }\n const pluginToolbarLabel = getPluginHeaderToolbarChipLabel(root);\n if (pluginToolbarLabel) {\n return pluginToolbarLabel;\n }\n const legacyTitle = queryFirstVisibleTitle(\n root,\n LEGACY_PAGE_TITLE_SELECTORS,\n );\n if (legacyTitle) {\n return legacyTitle;\n }\n }\n\n const normalized = normalizeDocumentTitle(doc.title);\n return normalized || 'Current page';\n}\n\nexport function getScreenContextRouteKind(\n pathname: string,\n search: string,\n): ScreenContextRouteKind {\n if (\n pathname.includes('/create/') ||\n pathname.includes('/template/') ||\n /\\/template\\/[^/]+/.test(pathname) ||\n (pathname.includes('/edit/') && pathname.includes('template'))\n ) {\n return 'template';\n }\n if (\n pathname.includes('/search') ||\n SEARCH_QUERY_PARAM_KEYS.some(key => new URLSearchParams(search).has(key)) ||\n search.includes('filters[') ||\n (pathname.includes('/catalog') && /[?&](query|term|filters\\[)/.test(search))\n ) {\n return 'search';\n }\n return 'default';\n}\n\nexport function getSearchQueryFromLocation(\n search: string,\n doc: Document = document,\n): string | undefined {\n const params = new URLSearchParams(\n search.startsWith('?') ? search : `?${search}`,\n );\n for (const key of SEARCH_QUERY_PARAM_KEYS) {\n const fromUrl = params.get(key)?.trim();\n if (fromUrl) {\n return fromUrl;\n }\n }\n\n const inputs = doc.querySelectorAll(SEARCH_INPUT_SELECTOR);\n for (const input of inputs) {\n if (!(input instanceof HTMLInputElement)) {\n continue;\n }\n if (!isElementVisible(input)) {\n continue;\n }\n const fromInput = input.value?.trim();\n if (fromInput) {\n return fromInput;\n }\n }\n\n return undefined;\n}\n\n/** @deprecated Prefer resolveScreenContextChipLabel */\nexport function getSearchQueryFromPage(): string | undefined {\n return getSearchQueryFromLocation(window.location.search);\n}\n\n/** Chip label for `/create/templates` gallery (filters in query string). */\nexport const SOFTWARE_TEMPLATES_LIST_CHIP_LABEL = 'Software templates';\n\nconst SOFTWARE_TEMPLATES_GALLERY_PATH = '/create/templates';\n\nfunction normalizePathname(pathname: string): string {\n const trimmed = pathname.trim();\n if (!trimmed || trimmed === '/') {\n return '/';\n }\n return trimmed.replace(/\\/+$/, '') || '/';\n}\n\nexport function isSoftwareTemplatesListPath(pathname: string): boolean {\n return normalizePathname(pathname) === SOFTWARE_TEMPLATES_GALLERY_PATH;\n}\n\nexport function parseSoftwareTemplateDetailSlug(\n pathname: string,\n): string | undefined {\n const normalized = normalizePathname(pathname);\n const prefix = `${SOFTWARE_TEMPLATES_GALLERY_PATH}/`;\n if (!normalized.startsWith(prefix)) {\n return undefined;\n }\n const segments = normalized.slice(prefix.length).split('/').filter(Boolean);\n if (segments.length < 2) {\n return undefined;\n }\n return segments[segments.length - 1];\n}\n\nexport function getSoftwareTemplateTitleFromDom(\n doc: Document = document,\n): string | undefined {\n const roots = getChromeRoots(doc);\n for (const root of roots) {\n const title = queryFirstVisibleTitle(\n root,\n TEMPLATE_INFO_CARD_TITLE_SELECTORS,\n );\n if (title) {\n return title;\n }\n }\n return undefined;\n}\n\n/**\n * Resolves chip text for software template gallery and template entity pages.\n */\nexport function resolveSoftwareTemplateChipLabel(\n pathname: string,\n doc: Document = document,\n): string | undefined {\n if (isSoftwareTemplatesListPath(pathname)) {\n return SOFTWARE_TEMPLATES_LIST_CHIP_LABEL;\n }\n const slug = parseSoftwareTemplateDetailSlug(pathname);\n if (!slug) {\n return undefined;\n }\n return getSoftwareTemplateTitleFromDom(doc) ?? slug;\n}\n\nexport function resolveScreenContextChipLabel(options: {\n pathname: string;\n search: string;\n document?: Document;\n}): ResolvedScreenContextLabel {\n const doc = options.document ?? document;\n const routeKind = getScreenContextRouteKind(options.pathname, options.search);\n\n if (routeKind === 'search') {\n const searchQuery = getSearchQueryFromLocation(options.search, doc);\n if (searchQuery) {\n return {\n label: truncateChipLabel(searchQuery),\n routeKind,\n searchQuery,\n };\n }\n }\n\n if (routeKind === 'template') {\n const softwareTemplateLabel = resolveSoftwareTemplateChipLabel(\n options.pathname,\n doc,\n );\n if (softwareTemplateLabel) {\n return {\n label: truncateChipLabel(softwareTemplateLabel),\n routeKind,\n };\n }\n }\n\n const pageTitle = getPageTitleFromDom(doc);\n return {\n label: truncateChipLabel(pageTitle),\n routeKind,\n searchQuery:\n routeKind === 'search'\n ? getSearchQueryFromLocation(options.search, doc)\n : undefined,\n };\n}\n\nexport function getScreenContextChipLabel(): string {\n return resolveScreenContextChipLabel({\n pathname: window.location.pathname,\n search: window.location.search,\n }).label;\n}\n\nexport type TooltipLine1Params = {\n chipLabel: string;\n routeKind: ScreenContextRouteKind;\n pathname?: string;\n searchQuery?: string;\n};\n\nexport function getScreenContextTooltipLine1Key(\n routeKind: ScreenContextRouteKind,\n pathname?: string,\n):\n | 'contextChip.tooltip.askAbout'\n | 'contextChip.tooltip.template'\n | 'contextChip.tooltip.search' {\n switch (routeKind) {\n case 'template':\n if (pathname && isSoftwareTemplatesListPath(pathname)) {\n return 'contextChip.tooltip.askAbout';\n }\n return 'contextChip.tooltip.template';\n case 'search':\n return 'contextChip.tooltip.search';\n default:\n return 'contextChip.tooltip.askAbout';\n }\n}\n\nexport function getScreenContextTooltipLine1Text(\n params: TooltipLine1Params,\n t: (key: string, options?: Record<string, string>) => string,\n): string {\n const label = params.chipLabel;\n const key = getScreenContextTooltipLine1Key(\n params.routeKind,\n params.pathname,\n );\n return t(key, { label });\n}\n\nexport type TooltipLine2Input = {\n domEnabled: boolean;\n screenshotsEnabled: boolean;\n supportsVision: boolean;\n};\n\nexport function getScreenContextTooltipLine2Key(\n input: TooltipLine2Input,\n): ScreenContextTooltipLine2Key {\n const { domEnabled, screenshotsEnabled, supportsVision } = input;\n\n const willSendDom = domEnabled;\n const willSendScreenshot = screenshotsEnabled && supportsVision;\n\n if (willSendDom && willSendScreenshot) {\n return 'fullContext';\n }\n\n if (!domEnabled && !screenshotsEnabled) {\n return 'adminLimited';\n }\n\n if (!domEnabled && screenshotsEnabled && supportsVision) {\n return 'screenshotOnly';\n }\n\n if (willSendDom) {\n const adminOff = !screenshotsEnabled;\n const noVision = !supportsVision;\n if (adminOff && noVision) {\n return 'textOnlyCombined';\n }\n if (noVision) {\n return 'textOnlyNoVision';\n }\n if (adminOff) {\n return 'textOnlyAdminScreenshotsOff';\n }\n }\n\n // DOM off, screenshots on, but model lacks vision — nothing attaches.\n if (!domEnabled && screenshotsEnabled && !supportsVision) {\n return 'domOffNoVision';\n }\n\n return 'adminLimited';\n}\n\nexport function shouldAttachScreenContext(\n input: ShouldAttachScreenContextInput,\n): ShouldAttachScreenContextResult {\n const baseGate =\n input.adminEnabled &&\n input.sharingEnabled &&\n !input.paused &&\n !input.isFullscreen;\n\n if (!baseGate) {\n return { attachDom: false, attachScreenshot: false };\n }\n\n return {\n attachDom: input.domEnabled,\n attachScreenshot: input.screenshotsEnabled && input.supportsVision,\n };\n}\n\nexport type BuildScreenContextAttachmentsInput =\n ShouldAttachScreenContextInput & {\n domExtractionMaxChars: number;\n };\n\nexport async function buildScreenContextAttachments(\n input: BuildScreenContextAttachmentsInput,\n): Promise<Attachment[]> {\n const { attachDom, attachScreenshot } = shouldAttachScreenContext(input);\n const attachments: Attachment[] = [];\n\n if (attachDom) {\n try {\n const domContext = extractPageContext({\n maxChars: input.domExtractionMaxChars,\n });\n if (domContext) {\n attachments.push({\n attachment_type: 'configuration',\n content_type: 'text/plain',\n content: domContext,\n });\n }\n } catch {\n // non-fatal\n }\n }\n\n if (attachScreenshot) {\n const result = await captureScreenshot({\n excludeSelector: '[data-screen-capture-exclude]',\n });\n if (result.success) {\n attachments.push({\n attachment_type: 'image',\n content_type: result.contentType,\n content: result.base64,\n });\n }\n }\n\n return attachments;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const REDACTED = "[REDACTED]";
|
|
2
|
+
const MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2
3
|
const SECRET_PATTERNS = [
|
|
3
4
|
// GitHub tokens (classic PAT, server-to-server, OAuth, fine-grained, user-to-server, refresh)
|
|
4
5
|
/gh[pousr]_[a-zA-Z0-9]{36,}/g,
|
|
@@ -61,6 +62,43 @@ function isSensitiveElement(el, doc) {
|
|
|
61
62
|
}
|
|
62
63
|
return false;
|
|
63
64
|
}
|
|
65
|
+
function sanitizeClonedDom(clonedDoc) {
|
|
66
|
+
clonedDoc.querySelectorAll("input, textarea").forEach((el) => {
|
|
67
|
+
if (isSensitiveElement(el, clonedDoc)) {
|
|
68
|
+
el.value = MASK;
|
|
69
|
+
} else {
|
|
70
|
+
const inp = el;
|
|
71
|
+
if (inp.value) {
|
|
72
|
+
inp.value = redactText(inp.value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const walker = clonedDoc.createTreeWalker(
|
|
77
|
+
clonedDoc.body,
|
|
78
|
+
NodeFilter.SHOW_TEXT
|
|
79
|
+
);
|
|
80
|
+
let node = walker.nextNode();
|
|
81
|
+
while (node) {
|
|
82
|
+
if (node.textContent) {
|
|
83
|
+
const redacted = redactText(node.textContent);
|
|
84
|
+
if (redacted !== node.textContent) {
|
|
85
|
+
node.textContent = redacted;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
node = walker.nextNode();
|
|
89
|
+
}
|
|
90
|
+
clonedDoc.querySelectorAll(
|
|
91
|
+
'[aria-label*="Hide secret" i], [aria-label*="Hide value" i]'
|
|
92
|
+
).forEach((btn) => {
|
|
93
|
+
const container = btn.closest('[class*="Box"], [class*="flex"]');
|
|
94
|
+
if (container) {
|
|
95
|
+
const textEl = container.querySelector("pre, code, span, p");
|
|
96
|
+
if (textEl?.textContent && textEl.textContent !== MASK) {
|
|
97
|
+
textEl.textContent = MASK;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
64
102
|
|
|
65
|
-
export { SAFE_LABEL_EXCLUSIONS, SECRET_PATTERNS, SENSITIVE_LABEL_PATTERN, isSensitiveElement, redactText };
|
|
103
|
+
export { SAFE_LABEL_EXCLUSIONS, SECRET_PATTERNS, SENSITIVE_LABEL_PATTERN, isSensitiveElement, redactText, sanitizeClonedDom };
|
|
66
104
|
//# sourceMappingURL=sensitive-data-redactor.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sensitive-data-redactor.esm.js","sources":["../../src/utils/sensitive-data-redactor.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst REDACTED = '[REDACTED]';\nconst MASK = '••••••••';\n\n/**\n * Regex patterns matching known secret/token formats.\n * Each pattern uses the global flag for replaceAll behavior.\n * WARNING: These regexes are stateful (/g). Reset lastIndex before calling\n * .test() or .exec() directly. Prefer using redactText() instead.\n */\nexport const SECRET_PATTERNS: RegExp[] = [\n // GitHub tokens (classic PAT, server-to-server, OAuth, fine-grained, user-to-server, refresh)\n /gh[pousr]_[a-zA-Z0-9]{36,}/g,\n /github_pat_[a-zA-Z0-9_]{22,}/g,\n // GitLab PAT\n /glpat-[a-zA-Z0-9\\-_]{20,}/g,\n // OpenAI / Anthropic API keys\n /sk-[a-zA-Z0-9-]{32,}/g,\n // AWS access key IDs\n /AKIA[0-9A-Z]{16}/g,\n // Slack tokens (app, bot, refresh, session, user)\n /xox[abrsp]-[0-9]+-[A-Za-z0-9]+/g,\n // Slack incoming webhooks\n /https:\\/\\/hooks\\.slack\\.com\\/services\\/T[A-Za-z0-9]+\\/B[A-Za-z0-9]+\\/[A-Za-z0-9]+/g,\n // npm access tokens\n /npm_[A-Za-z0-9]{36,}/g,\n // Google API keys\n /AIza[A-Za-z0-9_-]{35}/g,\n // Azure AD client secrets (v2 format: 3 chars + 7Q~ or 8Q~ + 31 chars)\n /[\\w~.-]{3}[78]Q~[\\w~.-]{31}/g,\n // JWTs\n /eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]*/g,\n // PEM private keys\n /-----BEGIN[A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,\n // Basic auth in URIs (e.g. https://user:pass@host)\n /\\w+:\\/\\/[^/\\s:]+:[^/\\s@]+@[^/\\s]+/g,\n // Bearer tokens\n /Bearer\\s+[A-Za-z0-9\\-._~+/]{20,}=*/g,\n];\n\n/**\n * Label text patterns that indicate a field holds sensitive content.\n */\nexport const SENSITIVE_LABEL_PATTERN =\n /secret|token|password|api.?key|credential|private.?key|access.?key/i;\n\n/**\n * Label patterns that are safe false-positives and should NOT trigger redaction.\n */\nexport const SAFE_LABEL_EXCLUSIONS =\n /token usage|token count|token limit|token budget/i;\n\n/**\n * Scans text for known secret patterns and replaces them with [REDACTED].\n * Used by DOM extraction to sanitize extracted text before sending.\n */\nexport function redactText(text: string): string {\n let result = text;\n for (const pattern of SECRET_PATTERNS) {\n pattern.lastIndex = 0;\n result = result.replace(pattern, REDACTED);\n }\n return result;\n}\n\n/**\n * Determines if an input/textarea element is likely holding a secret,\n * based on its type, aria-label, or associated label element.\n */\nexport function isSensitiveElement(el: Element, doc: Document): boolean {\n if ((el as HTMLInputElement).type === 'password') {\n return true;\n }\n\n for (const attr of ['aria-label', 'name', 'title', 'placeholder']) {\n const val = el.getAttribute(attr) || '';\n if (SENSITIVE_LABEL_PATTERN.test(val) && !SAFE_LABEL_EXCLUSIONS.test(val)) {\n return true;\n }\n }\n\n const id = el.getAttribute('id');\n if (id) {\n const label = doc.querySelector(`label[for=\"${CSS.escape(id)}\"]`);\n if (label?.textContent && SENSITIVE_LABEL_PATTERN.test(label.textContent)) {\n return !SAFE_LABEL_EXCLUSIONS.test(label.textContent);\n }\n }\n\n const parentLabel = el.closest('label');\n if (\n parentLabel?.textContent &&\n SENSITIVE_LABEL_PATTERN.test(parentLabel.textContent)\n ) {\n return !SAFE_LABEL_EXCLUSIONS.test(parentLabel.textContent);\n }\n\n return false;\n}\n\n/**\n * Sanitizes a cloned DOM before it is rendered to canvas.\n * Used in html2canvas onclone callback.\n * Mutates the clone — safe because it's not the live DOM.\n */\nexport function sanitizeClonedDom(clonedDoc: Document): void {\n // 1. Mask all sensitive inputs/textareas (password fields + label-detected)\n // 2. Regex-scan all remaining input/textarea values for token patterns\n clonedDoc.querySelectorAll('input, textarea').forEach(el => {\n if (isSensitiveElement(el, clonedDoc)) {\n (el as HTMLInputElement).value = MASK;\n } else {\n const inp = el as HTMLInputElement;\n if (inp.value) {\n inp.value = redactText(inp.value);\n }\n }\n });\n\n // 3. Regex scan all text nodes for token patterns\n const walker = clonedDoc.createTreeWalker(\n clonedDoc.body,\n NodeFilter.SHOW_TEXT,\n );\n let node = walker.nextNode();\n while (node) {\n if (node.textContent) {\n const redacted = redactText(node.textContent);\n if (redacted !== node.textContent) {\n node.textContent = redacted;\n }\n }\n node = walker.nextNode();\n }\n\n // 4. Handle visibility-toggle secrets (e.g. Akeyless show/hide pattern)\n clonedDoc\n .querySelectorAll(\n '[aria-label*=\"Hide secret\" i], [aria-label*=\"Hide value\" i]',\n )\n .forEach(btn => {\n const container = btn.closest('[class*=\"Box\"], [class*=\"flex\"]');\n if (container) {\n const textEl = container.querySelector('pre, code, span, p');\n if (textEl?.textContent && textEl.textContent !== MASK) {\n textEl.textContent = MASK;\n }\n }\n });\n}\n"],"names":[],"mappings":"AAgBA,MAAM,QAAA,GAAW,YAAA;
|
|
1
|
+
{"version":3,"file":"sensitive-data-redactor.esm.js","sources":["../../src/utils/sensitive-data-redactor.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst REDACTED = '[REDACTED]';\nconst MASK = '••••••••';\n\n/**\n * Regex patterns matching known secret/token formats.\n * Each pattern uses the global flag for replaceAll behavior.\n * WARNING: These regexes are stateful (/g). Reset lastIndex before calling\n * .test() or .exec() directly. Prefer using redactText() instead.\n */\nexport const SECRET_PATTERNS: RegExp[] = [\n // GitHub tokens (classic PAT, server-to-server, OAuth, fine-grained, user-to-server, refresh)\n /gh[pousr]_[a-zA-Z0-9]{36,}/g,\n /github_pat_[a-zA-Z0-9_]{22,}/g,\n // GitLab PAT\n /glpat-[a-zA-Z0-9\\-_]{20,}/g,\n // OpenAI / Anthropic API keys\n /sk-[a-zA-Z0-9-]{32,}/g,\n // AWS access key IDs\n /AKIA[0-9A-Z]{16}/g,\n // Slack tokens (app, bot, refresh, session, user)\n /xox[abrsp]-[0-9]+-[A-Za-z0-9]+/g,\n // Slack incoming webhooks\n /https:\\/\\/hooks\\.slack\\.com\\/services\\/T[A-Za-z0-9]+\\/B[A-Za-z0-9]+\\/[A-Za-z0-9]+/g,\n // npm access tokens\n /npm_[A-Za-z0-9]{36,}/g,\n // Google API keys\n /AIza[A-Za-z0-9_-]{35}/g,\n // Azure AD client secrets (v2 format: 3 chars + 7Q~ or 8Q~ + 31 chars)\n /[\\w~.-]{3}[78]Q~[\\w~.-]{31}/g,\n // JWTs\n /eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]*/g,\n // PEM private keys\n /-----BEGIN[A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,\n // Basic auth in URIs (e.g. https://user:pass@host)\n /\\w+:\\/\\/[^/\\s:]+:[^/\\s@]+@[^/\\s]+/g,\n // Bearer tokens\n /Bearer\\s+[A-Za-z0-9\\-._~+/]{20,}=*/g,\n];\n\n/**\n * Label text patterns that indicate a field holds sensitive content.\n */\nexport const SENSITIVE_LABEL_PATTERN =\n /secret|token|password|api.?key|credential|private.?key|access.?key/i;\n\n/**\n * Label patterns that are safe false-positives and should NOT trigger redaction.\n */\nexport const SAFE_LABEL_EXCLUSIONS =\n /token usage|token count|token limit|token budget/i;\n\n/**\n * Scans text for known secret patterns and replaces them with [REDACTED].\n * Used by DOM extraction to sanitize extracted text before sending.\n */\nexport function redactText(text: string): string {\n let result = text;\n for (const pattern of SECRET_PATTERNS) {\n pattern.lastIndex = 0;\n result = result.replace(pattern, REDACTED);\n }\n return result;\n}\n\n/**\n * Determines if an input/textarea element is likely holding a secret,\n * based on its type, aria-label, or associated label element.\n */\nexport function isSensitiveElement(el: Element, doc: Document): boolean {\n if ((el as HTMLInputElement).type === 'password') {\n return true;\n }\n\n for (const attr of ['aria-label', 'name', 'title', 'placeholder']) {\n const val = el.getAttribute(attr) || '';\n if (SENSITIVE_LABEL_PATTERN.test(val) && !SAFE_LABEL_EXCLUSIONS.test(val)) {\n return true;\n }\n }\n\n const id = el.getAttribute('id');\n if (id) {\n const label = doc.querySelector(`label[for=\"${CSS.escape(id)}\"]`);\n if (label?.textContent && SENSITIVE_LABEL_PATTERN.test(label.textContent)) {\n return !SAFE_LABEL_EXCLUSIONS.test(label.textContent);\n }\n }\n\n const parentLabel = el.closest('label');\n if (\n parentLabel?.textContent &&\n SENSITIVE_LABEL_PATTERN.test(parentLabel.textContent)\n ) {\n return !SAFE_LABEL_EXCLUSIONS.test(parentLabel.textContent);\n }\n\n return false;\n}\n\n/**\n * Sanitizes a cloned DOM before it is rendered to canvas.\n * Used in html2canvas onclone callback.\n * Mutates the clone — safe because it's not the live DOM.\n */\nexport function sanitizeClonedDom(clonedDoc: Document): void {\n // 1. Mask all sensitive inputs/textareas (password fields + label-detected)\n // 2. Regex-scan all remaining input/textarea values for token patterns\n clonedDoc.querySelectorAll('input, textarea').forEach(el => {\n if (isSensitiveElement(el, clonedDoc)) {\n (el as HTMLInputElement).value = MASK;\n } else {\n const inp = el as HTMLInputElement;\n if (inp.value) {\n inp.value = redactText(inp.value);\n }\n }\n });\n\n // 3. Regex scan all text nodes for token patterns\n const walker = clonedDoc.createTreeWalker(\n clonedDoc.body,\n NodeFilter.SHOW_TEXT,\n );\n let node = walker.nextNode();\n while (node) {\n if (node.textContent) {\n const redacted = redactText(node.textContent);\n if (redacted !== node.textContent) {\n node.textContent = redacted;\n }\n }\n node = walker.nextNode();\n }\n\n // 4. Handle visibility-toggle secrets (e.g. Akeyless show/hide pattern)\n clonedDoc\n .querySelectorAll(\n '[aria-label*=\"Hide secret\" i], [aria-label*=\"Hide value\" i]',\n )\n .forEach(btn => {\n const container = btn.closest('[class*=\"Box\"], [class*=\"flex\"]');\n if (container) {\n const textEl = container.querySelector('pre, code, span, p');\n if (textEl?.textContent && textEl.textContent !== MASK) {\n textEl.textContent = MASK;\n }\n }\n });\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/backstage-plugin-intelligent-assistant",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"main": "./dist/index.esm.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"@patternfly/react-icons": "^6.5.0",
|
|
112
112
|
"@patternfly/react-table": "^6.4.1",
|
|
113
113
|
"@red-hat-developer-hub/backstage-plugin-app-react": "^1.0.0",
|
|
114
|
-
"@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common": "^5.
|
|
114
|
+
"@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common": "^5.3.1",
|
|
115
115
|
"@red-hat-developer-hub/backstage-plugin-theme": "^0.14.10",
|
|
116
116
|
"@tanstack/react-query": "^5.59.15",
|
|
117
117
|
"html2canvas-pro": "^1.5.0",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"@testing-library/jest-dom": "^6.0.0",
|
|
136
136
|
"@testing-library/react": "^15.0.0",
|
|
137
137
|
"@testing-library/user-event": "14.6.1",
|
|
138
|
-
"prettier": "3.9.
|
|
138
|
+
"prettier": "3.9.8",
|
|
139
139
|
"react": "^18.0.0",
|
|
140
140
|
"react-dom": "^18.0.0",
|
|
141
141
|
"react-router-dom": "^6.30.6"
|