spaps-issue-reporting-react 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/screenshot-capture.ts
31
+ var screenshot_capture_exports = {};
32
+ __export(screenshot_capture_exports, {
33
+ ATTACHMENT_MAX_BYTES: () => ATTACHMENT_MAX_BYTES,
34
+ SCREENSHOT_CAPTURE_REDACTION_TEXT: () => SCREENSHOT_CAPTURE_REDACTION_TEXT,
35
+ captureScreenshot: () => captureScreenshot,
36
+ maskScreenshotCaptureDocument: () => maskScreenshotCaptureDocument,
37
+ resetHtml2CanvasCache: () => resetHtml2CanvasCache
38
+ });
39
+ module.exports = __toCommonJS(screenshot_capture_exports);
40
+ var DEFAULT_EXCLUDE_SELECTORS = [
41
+ "[data-spaps-screenshot-exclude]",
42
+ "[data-spaps-private]"
43
+ ];
44
+ var DEFAULT_MASK_SELECTORS = [
45
+ "input",
46
+ "option",
47
+ "select",
48
+ "textarea",
49
+ '[contenteditable="true"]'
50
+ ];
51
+ var SCREENSHOT_CAPTURE_REDACTION_TEXT = "[redacted]";
52
+ var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
53
+ var MIME_TO_EXTENSION = {
54
+ "image/png": "png",
55
+ "image/jpeg": "jpg",
56
+ "image/webp": "webp"
57
+ };
58
+ var html2canvasPromise = null;
59
+ function loadHtml2Canvas() {
60
+ if (!html2canvasPromise) {
61
+ html2canvasPromise = import("html2canvas");
62
+ }
63
+ return html2canvasPromise;
64
+ }
65
+ function resolveFilename(config, context) {
66
+ const imageType = config.imageType ?? "image/png";
67
+ const ext = MIME_TO_EXTENSION[imageType] ?? "png";
68
+ if (typeof config.filename === "function") {
69
+ return config.filename(context);
70
+ }
71
+ if (typeof config.filename === "string") {
72
+ return config.filename;
73
+ }
74
+ return `screenshot-${context.target.componentKey}-${Date.now()}.${ext}`;
75
+ }
76
+ function resolveRootElement(config) {
77
+ if (typeof config.rootElement === "function") {
78
+ return config.rootElement();
79
+ }
80
+ if (typeof HTMLElement !== "undefined" && config.rootElement instanceof HTMLElement) {
81
+ return config.rootElement;
82
+ }
83
+ return document.documentElement;
84
+ }
85
+ function buildIgnoreSelectors(config) {
86
+ const selectors = [...DEFAULT_EXCLUDE_SELECTORS];
87
+ if (config.excludeSelectors) {
88
+ selectors.push(...config.excludeSelectors.map((selector) => selector.trim()));
89
+ }
90
+ return selectors.filter(Boolean);
91
+ }
92
+ function safelyMatchesSelector(element, selector) {
93
+ try {
94
+ return element.matches(selector);
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ function buildIgnoreElementPredicate(config) {
100
+ const selectors = buildIgnoreSelectors(config);
101
+ return (element) => selectors.some((selector) => safelyMatchesSelector(element, selector));
102
+ }
103
+ function canvasToBlobAsync(canvas, mimeType, quality) {
104
+ return new Promise((resolve) => {
105
+ canvas.toBlob(resolve, mimeType, quality);
106
+ });
107
+ }
108
+ async function canvasToSizedBlob(canvas, imageType, quality, maxBytes) {
109
+ const initial = await canvasToBlobAsync(canvas, imageType, quality);
110
+ if (initial && initial.size <= maxBytes) {
111
+ return { blob: initial, scaled: false };
112
+ }
113
+ const fallbackTypes = imageType === "image/png" ? ["image/webp", "image/jpeg"] : [imageType, "image/webp", "image/jpeg"];
114
+ let currentCanvas = canvas;
115
+ let scaled = false;
116
+ for (const scale of [0.85, 0.7, 0.55, 0.4]) {
117
+ currentCanvas = scaleCanvas(currentCanvas, scale);
118
+ scaled = true;
119
+ for (const format of fallbackTypes) {
120
+ for (const candidateQuality of [0.86, 0.72, 0.58, 0.44, 0.32]) {
121
+ const attempt = await canvasToBlobAsync(
122
+ currentCanvas,
123
+ format,
124
+ candidateQuality
125
+ );
126
+ if (attempt && attempt.size <= maxBytes) {
127
+ return { blob: attempt, scaled };
128
+ }
129
+ }
130
+ }
131
+ }
132
+ throw new Error(
133
+ `Screenshot exceeds ${Math.round(maxBytes / 1024 / 1024)} MiB after compression`
134
+ );
135
+ }
136
+ async function captureScreenshot(config, context) {
137
+ if (!config.enabled) {
138
+ throw new Error("Screenshot capture is not enabled");
139
+ }
140
+ if (!canUseBrowserCapture()) {
141
+ throw new Error("Browser screenshot capture is unavailable");
142
+ }
143
+ const { default: html2canvas } = await loadHtml2Canvas();
144
+ const rootElement = resolveRootElement(config);
145
+ if (!rootElement) {
146
+ throw new Error("Screenshot root element not found");
147
+ }
148
+ const ignoreElements = buildIgnoreElementPredicate(config);
149
+ const imageType = config.imageType ?? "image/png";
150
+ const maxBytes = Math.min(config.maxBytes ?? ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_BYTES);
151
+ const viewport = readViewport();
152
+ const rect = resolveCaptureRect(config, viewport);
153
+ const pixelRatio = Math.min(
154
+ Math.max(config.pixelRatio ?? window.devicePixelRatio ?? 1, 1),
155
+ 2
156
+ );
157
+ const ignoreSelectors = buildIgnoreSelectors(config);
158
+ const canvas = await html2canvas(rootElement, {
159
+ useCORS: true,
160
+ allowTaint: false,
161
+ backgroundColor: null,
162
+ logging: false,
163
+ ignoreElements,
164
+ onclone: (clonedDocument) => {
165
+ maskScreenshotCaptureDocument(
166
+ clonedDocument,
167
+ ignoreSelectors,
168
+ config.maskSelectors
169
+ );
170
+ },
171
+ removeContainer: true,
172
+ scale: pixelRatio,
173
+ scrollX: window.scrollX,
174
+ scrollY: window.scrollY,
175
+ width: rect.width,
176
+ height: rect.height,
177
+ windowWidth: viewport.width,
178
+ windowHeight: viewport.height,
179
+ x: rect.left + window.scrollX,
180
+ y: rect.top + window.scrollY
181
+ });
182
+ const output = await canvasToSizedBlob(
183
+ canvas,
184
+ imageType,
185
+ config.quality,
186
+ maxBytes
187
+ );
188
+ const filename = resolveFilename(config, context);
189
+ return {
190
+ blob: new Blob([output.blob], { type: output.blob.type || imageType }),
191
+ filename: replaceFilenameExtension(
192
+ filename,
193
+ MIME_TO_EXTENSION[output.blob.type || imageType] ?? MIME_TO_EXTENSION[imageType]
194
+ ),
195
+ mimeType: output.blob.type || imageType,
196
+ byteSize: output.blob.size,
197
+ rect,
198
+ scaled: output.scaled
199
+ };
200
+ }
201
+ function resetHtml2CanvasCache() {
202
+ html2canvasPromise = null;
203
+ }
204
+ function maskScreenshotCaptureDocument(clonedDocument, excludeSelectors = DEFAULT_EXCLUDE_SELECTORS, maskSelectors = DEFAULT_MASK_SELECTORS) {
205
+ clonedDocument.querySelectorAll([...excludeSelectors, "script"].join(",")).forEach((element) => element.remove());
206
+ clonedDocument.querySelectorAll(maskSelectors.join(",")).forEach(maskCaptureElement);
207
+ redactSensitiveTextNodes(clonedDocument.body ?? clonedDocument.documentElement);
208
+ }
209
+ function canUseBrowserCapture() {
210
+ return typeof window !== "undefined" && typeof document !== "undefined" && typeof Blob !== "undefined" && typeof document.createElement === "function";
211
+ }
212
+ function readViewport() {
213
+ return {
214
+ width: Math.max(
215
+ 1,
216
+ Math.round(window.innerWidth || document.documentElement.clientWidth || 1)
217
+ ),
218
+ height: Math.max(
219
+ 1,
220
+ Math.round(window.innerHeight || document.documentElement.clientHeight || 1)
221
+ )
222
+ };
223
+ }
224
+ function resolveCaptureRect(config, viewport) {
225
+ const configuredRect = typeof config.rect === "function" ? config.rect() : config.rect;
226
+ const source = configuredRect && configuredRect.width > 0 && configuredRect.height > 0 ? configuredRect : { top: 0, left: 0, width: viewport.width, height: viewport.height };
227
+ const left = clamp(Math.round(source.left), 0, viewport.width - 1);
228
+ const top = clamp(Math.round(source.top), 0, viewport.height - 1);
229
+ const right = clamp(
230
+ Math.round(source.left + source.width),
231
+ left + 1,
232
+ viewport.width
233
+ );
234
+ const bottom = clamp(
235
+ Math.round(source.top + source.height),
236
+ top + 1,
237
+ viewport.height
238
+ );
239
+ return {
240
+ left,
241
+ top,
242
+ width: right - left,
243
+ height: bottom - top
244
+ };
245
+ }
246
+ function clamp(value, min, max) {
247
+ return Math.min(Math.max(value, min), max);
248
+ }
249
+ function maskCaptureElement(element) {
250
+ if (element instanceof HTMLInputElement) {
251
+ element.value = "";
252
+ element.setAttribute("value", "");
253
+ element.setAttribute("placeholder", SCREENSHOT_CAPTURE_REDACTION_TEXT);
254
+ } else if (element instanceof HTMLTextAreaElement) {
255
+ element.value = "";
256
+ element.textContent = SCREENSHOT_CAPTURE_REDACTION_TEXT;
257
+ element.setAttribute("placeholder", SCREENSHOT_CAPTURE_REDACTION_TEXT);
258
+ } else if (element instanceof HTMLSelectElement) {
259
+ element.querySelectorAll("option").forEach((option) => {
260
+ option.textContent = SCREENSHOT_CAPTURE_REDACTION_TEXT;
261
+ });
262
+ } else {
263
+ element.textContent = SCREENSHOT_CAPTURE_REDACTION_TEXT;
264
+ }
265
+ if (element instanceof HTMLElement) {
266
+ element.setAttribute("aria-label", SCREENSHOT_CAPTURE_REDACTION_TEXT);
267
+ element.style.color = "#1d1914";
268
+ element.style.backgroundColor = "#d8d0c1";
269
+ }
270
+ }
271
+ function redactSensitiveTextNodes(root) {
272
+ if (!root) {
273
+ return;
274
+ }
275
+ const ownerDocument = root.ownerDocument ?? document;
276
+ const walker = ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
277
+ let node = walker.nextNode();
278
+ while (node) {
279
+ if (node.textContent) {
280
+ node.textContent = redactSensitiveText(node.textContent);
281
+ }
282
+ node = walker.nextNode();
283
+ }
284
+ }
285
+ function redactSensitiveText(value) {
286
+ return value.replace(
287
+ /\b((?:access|refresh|id)_token)=([^&#\s]+)/gi,
288
+ (_, key, token) => `${key}=${redactValue(token)}`
289
+ ).replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/\b[ps]k_(?:live|test)_[A-Za-z0-9_=-]+/g, SCREENSHOT_CAPTURE_REDACTION_TEXT).replace(/\b(?:token|secret|password|signature)[_-]?[A-Za-z0-9._~+/=-]{8,}/gi, SCREENSHOT_CAPTURE_REDACTION_TEXT);
290
+ }
291
+ function redactValue(value) {
292
+ return value ? SCREENSHOT_CAPTURE_REDACTION_TEXT : value;
293
+ }
294
+ function scaleCanvas(source, ratio) {
295
+ const canvas = document.createElement("canvas");
296
+ canvas.width = Math.max(1, Math.round(source.width * ratio));
297
+ canvas.height = Math.max(1, Math.round(source.height * ratio));
298
+ const context = canvas.getContext("2d");
299
+ if (!context) {
300
+ throw new Error("Canvas 2D context is unavailable");
301
+ }
302
+ context.drawImage(source, 0, 0, canvas.width, canvas.height);
303
+ return canvas;
304
+ }
305
+ function replaceFilenameExtension(filename, extension) {
306
+ return filename.replace(/\.(png|jpe?g|webp)$/i, `.${extension}`);
307
+ }
308
+ // Annotate the CommonJS export names for ESM import in node:
309
+ 0 && (module.exports = {
310
+ ATTACHMENT_MAX_BYTES,
311
+ SCREENSHOT_CAPTURE_REDACTION_TEXT,
312
+ captureScreenshot,
313
+ maskScreenshotCaptureDocument,
314
+ resetHtml2CanvasCache
315
+ });
@@ -0,0 +1,276 @@
1
+ // src/screenshot-capture.ts
2
+ var DEFAULT_EXCLUDE_SELECTORS = [
3
+ "[data-spaps-screenshot-exclude]",
4
+ "[data-spaps-private]"
5
+ ];
6
+ var DEFAULT_MASK_SELECTORS = [
7
+ "input",
8
+ "option",
9
+ "select",
10
+ "textarea",
11
+ '[contenteditable="true"]'
12
+ ];
13
+ var SCREENSHOT_CAPTURE_REDACTION_TEXT = "[redacted]";
14
+ var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
15
+ var MIME_TO_EXTENSION = {
16
+ "image/png": "png",
17
+ "image/jpeg": "jpg",
18
+ "image/webp": "webp"
19
+ };
20
+ var html2canvasPromise = null;
21
+ function loadHtml2Canvas() {
22
+ if (!html2canvasPromise) {
23
+ html2canvasPromise = import("html2canvas");
24
+ }
25
+ return html2canvasPromise;
26
+ }
27
+ function resolveFilename(config, context) {
28
+ const imageType = config.imageType ?? "image/png";
29
+ const ext = MIME_TO_EXTENSION[imageType] ?? "png";
30
+ if (typeof config.filename === "function") {
31
+ return config.filename(context);
32
+ }
33
+ if (typeof config.filename === "string") {
34
+ return config.filename;
35
+ }
36
+ return `screenshot-${context.target.componentKey}-${Date.now()}.${ext}`;
37
+ }
38
+ function resolveRootElement(config) {
39
+ if (typeof config.rootElement === "function") {
40
+ return config.rootElement();
41
+ }
42
+ if (typeof HTMLElement !== "undefined" && config.rootElement instanceof HTMLElement) {
43
+ return config.rootElement;
44
+ }
45
+ return document.documentElement;
46
+ }
47
+ function buildIgnoreSelectors(config) {
48
+ const selectors = [...DEFAULT_EXCLUDE_SELECTORS];
49
+ if (config.excludeSelectors) {
50
+ selectors.push(...config.excludeSelectors.map((selector) => selector.trim()));
51
+ }
52
+ return selectors.filter(Boolean);
53
+ }
54
+ function safelyMatchesSelector(element, selector) {
55
+ try {
56
+ return element.matches(selector);
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ function buildIgnoreElementPredicate(config) {
62
+ const selectors = buildIgnoreSelectors(config);
63
+ return (element) => selectors.some((selector) => safelyMatchesSelector(element, selector));
64
+ }
65
+ function canvasToBlobAsync(canvas, mimeType, quality) {
66
+ return new Promise((resolve) => {
67
+ canvas.toBlob(resolve, mimeType, quality);
68
+ });
69
+ }
70
+ async function canvasToSizedBlob(canvas, imageType, quality, maxBytes) {
71
+ const initial = await canvasToBlobAsync(canvas, imageType, quality);
72
+ if (initial && initial.size <= maxBytes) {
73
+ return { blob: initial, scaled: false };
74
+ }
75
+ const fallbackTypes = imageType === "image/png" ? ["image/webp", "image/jpeg"] : [imageType, "image/webp", "image/jpeg"];
76
+ let currentCanvas = canvas;
77
+ let scaled = false;
78
+ for (const scale of [0.85, 0.7, 0.55, 0.4]) {
79
+ currentCanvas = scaleCanvas(currentCanvas, scale);
80
+ scaled = true;
81
+ for (const format of fallbackTypes) {
82
+ for (const candidateQuality of [0.86, 0.72, 0.58, 0.44, 0.32]) {
83
+ const attempt = await canvasToBlobAsync(
84
+ currentCanvas,
85
+ format,
86
+ candidateQuality
87
+ );
88
+ if (attempt && attempt.size <= maxBytes) {
89
+ return { blob: attempt, scaled };
90
+ }
91
+ }
92
+ }
93
+ }
94
+ throw new Error(
95
+ `Screenshot exceeds ${Math.round(maxBytes / 1024 / 1024)} MiB after compression`
96
+ );
97
+ }
98
+ async function captureScreenshot(config, context) {
99
+ if (!config.enabled) {
100
+ throw new Error("Screenshot capture is not enabled");
101
+ }
102
+ if (!canUseBrowserCapture()) {
103
+ throw new Error("Browser screenshot capture is unavailable");
104
+ }
105
+ const { default: html2canvas } = await loadHtml2Canvas();
106
+ const rootElement = resolveRootElement(config);
107
+ if (!rootElement) {
108
+ throw new Error("Screenshot root element not found");
109
+ }
110
+ const ignoreElements = buildIgnoreElementPredicate(config);
111
+ const imageType = config.imageType ?? "image/png";
112
+ const maxBytes = Math.min(config.maxBytes ?? ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_BYTES);
113
+ const viewport = readViewport();
114
+ const rect = resolveCaptureRect(config, viewport);
115
+ const pixelRatio = Math.min(
116
+ Math.max(config.pixelRatio ?? window.devicePixelRatio ?? 1, 1),
117
+ 2
118
+ );
119
+ const ignoreSelectors = buildIgnoreSelectors(config);
120
+ const canvas = await html2canvas(rootElement, {
121
+ useCORS: true,
122
+ allowTaint: false,
123
+ backgroundColor: null,
124
+ logging: false,
125
+ ignoreElements,
126
+ onclone: (clonedDocument) => {
127
+ maskScreenshotCaptureDocument(
128
+ clonedDocument,
129
+ ignoreSelectors,
130
+ config.maskSelectors
131
+ );
132
+ },
133
+ removeContainer: true,
134
+ scale: pixelRatio,
135
+ scrollX: window.scrollX,
136
+ scrollY: window.scrollY,
137
+ width: rect.width,
138
+ height: rect.height,
139
+ windowWidth: viewport.width,
140
+ windowHeight: viewport.height,
141
+ x: rect.left + window.scrollX,
142
+ y: rect.top + window.scrollY
143
+ });
144
+ const output = await canvasToSizedBlob(
145
+ canvas,
146
+ imageType,
147
+ config.quality,
148
+ maxBytes
149
+ );
150
+ const filename = resolveFilename(config, context);
151
+ return {
152
+ blob: new Blob([output.blob], { type: output.blob.type || imageType }),
153
+ filename: replaceFilenameExtension(
154
+ filename,
155
+ MIME_TO_EXTENSION[output.blob.type || imageType] ?? MIME_TO_EXTENSION[imageType]
156
+ ),
157
+ mimeType: output.blob.type || imageType,
158
+ byteSize: output.blob.size,
159
+ rect,
160
+ scaled: output.scaled
161
+ };
162
+ }
163
+ function resetHtml2CanvasCache() {
164
+ html2canvasPromise = null;
165
+ }
166
+ function maskScreenshotCaptureDocument(clonedDocument, excludeSelectors = DEFAULT_EXCLUDE_SELECTORS, maskSelectors = DEFAULT_MASK_SELECTORS) {
167
+ clonedDocument.querySelectorAll([...excludeSelectors, "script"].join(",")).forEach((element) => element.remove());
168
+ clonedDocument.querySelectorAll(maskSelectors.join(",")).forEach(maskCaptureElement);
169
+ redactSensitiveTextNodes(clonedDocument.body ?? clonedDocument.documentElement);
170
+ }
171
+ function canUseBrowserCapture() {
172
+ return typeof window !== "undefined" && typeof document !== "undefined" && typeof Blob !== "undefined" && typeof document.createElement === "function";
173
+ }
174
+ function readViewport() {
175
+ return {
176
+ width: Math.max(
177
+ 1,
178
+ Math.round(window.innerWidth || document.documentElement.clientWidth || 1)
179
+ ),
180
+ height: Math.max(
181
+ 1,
182
+ Math.round(window.innerHeight || document.documentElement.clientHeight || 1)
183
+ )
184
+ };
185
+ }
186
+ function resolveCaptureRect(config, viewport) {
187
+ const configuredRect = typeof config.rect === "function" ? config.rect() : config.rect;
188
+ const source = configuredRect && configuredRect.width > 0 && configuredRect.height > 0 ? configuredRect : { top: 0, left: 0, width: viewport.width, height: viewport.height };
189
+ const left = clamp(Math.round(source.left), 0, viewport.width - 1);
190
+ const top = clamp(Math.round(source.top), 0, viewport.height - 1);
191
+ const right = clamp(
192
+ Math.round(source.left + source.width),
193
+ left + 1,
194
+ viewport.width
195
+ );
196
+ const bottom = clamp(
197
+ Math.round(source.top + source.height),
198
+ top + 1,
199
+ viewport.height
200
+ );
201
+ return {
202
+ left,
203
+ top,
204
+ width: right - left,
205
+ height: bottom - top
206
+ };
207
+ }
208
+ function clamp(value, min, max) {
209
+ return Math.min(Math.max(value, min), max);
210
+ }
211
+ function maskCaptureElement(element) {
212
+ if (element instanceof HTMLInputElement) {
213
+ element.value = "";
214
+ element.setAttribute("value", "");
215
+ element.setAttribute("placeholder", SCREENSHOT_CAPTURE_REDACTION_TEXT);
216
+ } else if (element instanceof HTMLTextAreaElement) {
217
+ element.value = "";
218
+ element.textContent = SCREENSHOT_CAPTURE_REDACTION_TEXT;
219
+ element.setAttribute("placeholder", SCREENSHOT_CAPTURE_REDACTION_TEXT);
220
+ } else if (element instanceof HTMLSelectElement) {
221
+ element.querySelectorAll("option").forEach((option) => {
222
+ option.textContent = SCREENSHOT_CAPTURE_REDACTION_TEXT;
223
+ });
224
+ } else {
225
+ element.textContent = SCREENSHOT_CAPTURE_REDACTION_TEXT;
226
+ }
227
+ if (element instanceof HTMLElement) {
228
+ element.setAttribute("aria-label", SCREENSHOT_CAPTURE_REDACTION_TEXT);
229
+ element.style.color = "#1d1914";
230
+ element.style.backgroundColor = "#d8d0c1";
231
+ }
232
+ }
233
+ function redactSensitiveTextNodes(root) {
234
+ if (!root) {
235
+ return;
236
+ }
237
+ const ownerDocument = root.ownerDocument ?? document;
238
+ const walker = ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
239
+ let node = walker.nextNode();
240
+ while (node) {
241
+ if (node.textContent) {
242
+ node.textContent = redactSensitiveText(node.textContent);
243
+ }
244
+ node = walker.nextNode();
245
+ }
246
+ }
247
+ function redactSensitiveText(value) {
248
+ return value.replace(
249
+ /\b((?:access|refresh|id)_token)=([^&#\s]+)/gi,
250
+ (_, key, token) => `${key}=${redactValue(token)}`
251
+ ).replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/\b[ps]k_(?:live|test)_[A-Za-z0-9_=-]+/g, SCREENSHOT_CAPTURE_REDACTION_TEXT).replace(/\b(?:token|secret|password|signature)[_-]?[A-Za-z0-9._~+/=-]{8,}/gi, SCREENSHOT_CAPTURE_REDACTION_TEXT);
252
+ }
253
+ function redactValue(value) {
254
+ return value ? SCREENSHOT_CAPTURE_REDACTION_TEXT : value;
255
+ }
256
+ function scaleCanvas(source, ratio) {
257
+ const canvas = document.createElement("canvas");
258
+ canvas.width = Math.max(1, Math.round(source.width * ratio));
259
+ canvas.height = Math.max(1, Math.round(source.height * ratio));
260
+ const context = canvas.getContext("2d");
261
+ if (!context) {
262
+ throw new Error("Canvas 2D context is unavailable");
263
+ }
264
+ context.drawImage(source, 0, 0, canvas.width, canvas.height);
265
+ return canvas;
266
+ }
267
+ function replaceFilenameExtension(filename, extension) {
268
+ return filename.replace(/\.(png|jpe?g|webp)$/i, `.${extension}`);
269
+ }
270
+ export {
271
+ ATTACHMENT_MAX_BYTES,
272
+ SCREENSHOT_CAPTURE_REDACTION_TEXT,
273
+ captureScreenshot,
274
+ maskScreenshotCaptureDocument,
275
+ resetHtml2CanvasCache
276
+ };