@bugspotter/sdk 0.3.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +200 -0
- package/README.md +18 -16
- package/SECURITY.md +65 -0
- package/dist/bugspotter.min.js +2 -1
- package/dist/bugspotter.min.js.map +1 -0
- package/dist/capture/console.js +2 -2
- package/dist/capture/network.js +2 -2
- package/dist/core/offline-queue.d.ts +13 -0
- package/dist/core/offline-queue.js +49 -4
- package/dist/core/transport.js +20 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1460 -1178
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/utils/config-validator.js +6 -0
- package/dist/utils/sanitize-patterns.d.ts +3 -76
- package/dist/utils/sanitize-patterns.js +18 -216
- package/dist/utils/url-helpers.d.ts +2 -25
- package/dist/utils/url-helpers.js +10 -61
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/widget/button.d.ts +10 -0
- package/dist/widget/button.js +200 -3
- package/docs/CDN.md +5 -5
- package/eslint.config.js +10 -0
- package/package.json +15 -4
- package/release_notes.md +4 -0
- package/rollup.config.js +1 -1
- package/tsconfig.cjs.json +1 -1
- package/dist/core/circular-buffer.d.ts +0 -42
- package/dist/core/circular-buffer.js +0 -80
package/dist/index.esm.js
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
5
|
+
purpose with or without fee is hereby granted.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
9
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
11
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
12
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
14
|
+
***************************************************************************** */
|
|
15
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function __rest$1(s, e) {
|
|
19
|
+
var t = {};
|
|
20
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
21
|
+
t[p] = s[p];
|
|
22
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
23
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
24
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
25
|
+
t[p[i]] = s[p[i]];
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function __awaiter$1(thisArg, _arguments, P, generator) {
|
|
31
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
32
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
33
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
34
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
35
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
36
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
41
|
+
var e = new Error(message);
|
|
42
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
43
|
+
};
|
|
44
|
+
|
|
1
45
|
// Professional bug report icon SVG
|
|
2
46
|
const DEFAULT_SVG_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
3
47
|
<path d="M8 2v4"/>
|
|
@@ -34,6 +78,104 @@ const BUTTON_STYLES = {
|
|
|
34
78
|
active: 'scale(0.95)',
|
|
35
79
|
},
|
|
36
80
|
};
|
|
81
|
+
// SVG sanitization whitelists (module-level for performance)
|
|
82
|
+
const SAFE_SVG_TAGS = new Set([
|
|
83
|
+
'svg',
|
|
84
|
+
'g',
|
|
85
|
+
'path',
|
|
86
|
+
'circle',
|
|
87
|
+
'rect',
|
|
88
|
+
'line',
|
|
89
|
+
'polyline',
|
|
90
|
+
'polygon',
|
|
91
|
+
'ellipse',
|
|
92
|
+
'text',
|
|
93
|
+
'tspan',
|
|
94
|
+
// SECURITY: 'use' deliberately excluded - requires href/xlink:href attributes which pose XSS risks
|
|
95
|
+
// and are not in the attribute whitelist, making <use> non-functional anyway
|
|
96
|
+
'symbol',
|
|
97
|
+
'defs',
|
|
98
|
+
'marker',
|
|
99
|
+
'lineargradient', // lowercase to match tagName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
|
|
100
|
+
'radialgradient', // lowercase to match tagName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
|
|
101
|
+
'stop',
|
|
102
|
+
'clippath', // lowercase to match tagName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
|
|
103
|
+
'mask',
|
|
104
|
+
// SECURITY: 'image' deliberately excluded - requires href/xlink:href attributes which pose XSS risks
|
|
105
|
+
// and are not in the attribute whitelist, making <image> non-functional anyway
|
|
106
|
+
// SECURITY: foreignObject deliberately excluded - allows embedding arbitrary HTML/XML
|
|
107
|
+
// and can bypass SVG sanitization (e.g., <foreignObject><body><script>...</script></body></foreignObject>)
|
|
108
|
+
]);
|
|
109
|
+
const SAFE_SVG_ATTRIBUTES = new Set([
|
|
110
|
+
'id',
|
|
111
|
+
'class',
|
|
112
|
+
// SECURITY: 'style' deliberately excluded - can enable CSS-based attacks:
|
|
113
|
+
// - expression() in older browsers
|
|
114
|
+
// - url() with javascript:/data: URIs
|
|
115
|
+
// - @import with malicious stylesheets
|
|
116
|
+
// - CSS data exfiltration
|
|
117
|
+
// Use specific styling attributes (fill, stroke, opacity, etc.) instead
|
|
118
|
+
'd',
|
|
119
|
+
'cx',
|
|
120
|
+
'cy',
|
|
121
|
+
'r',
|
|
122
|
+
'rx',
|
|
123
|
+
'ry',
|
|
124
|
+
'x',
|
|
125
|
+
'y',
|
|
126
|
+
'x1',
|
|
127
|
+
'y1',
|
|
128
|
+
'x2',
|
|
129
|
+
'y2',
|
|
130
|
+
'width',
|
|
131
|
+
'height',
|
|
132
|
+
'viewbox', // lowercase to match attrName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
|
|
133
|
+
'xmlns',
|
|
134
|
+
'fill',
|
|
135
|
+
'stroke',
|
|
136
|
+
'stroke-width',
|
|
137
|
+
'stroke-linecap',
|
|
138
|
+
'stroke-linejoin',
|
|
139
|
+
'opacity',
|
|
140
|
+
'fill-opacity',
|
|
141
|
+
'stroke-opacity',
|
|
142
|
+
'transform',
|
|
143
|
+
'points',
|
|
144
|
+
'text-anchor',
|
|
145
|
+
'font-size',
|
|
146
|
+
'font-family',
|
|
147
|
+
'font-weight',
|
|
148
|
+
'offset',
|
|
149
|
+
'stop-color',
|
|
150
|
+
'stop-opacity',
|
|
151
|
+
'clip-path',
|
|
152
|
+
'mask', // Used to reference mask definitions: mask="url(#maskId)"
|
|
153
|
+
]);
|
|
154
|
+
/**
|
|
155
|
+
* Check if an attribute value contains dangerous patterns
|
|
156
|
+
* Uses simple string matching instead of regex for better performance and clarity
|
|
157
|
+
*/
|
|
158
|
+
const isDangerousAttributeValue = (value) => {
|
|
159
|
+
const lowerValue = value.toLowerCase();
|
|
160
|
+
// Dangerous protocol checks
|
|
161
|
+
if (lowerValue.includes('javascript:'))
|
|
162
|
+
return true;
|
|
163
|
+
if (lowerValue.includes('vbscript:'))
|
|
164
|
+
return true;
|
|
165
|
+
// SECURITY: Block ALL data: URIs by default
|
|
166
|
+
// data:text/html, data:application/javascript, data:image/svg+xml can all execute scripts
|
|
167
|
+
// Even data:text/javascript or data URIs with embedded scripts are dangerous
|
|
168
|
+
if (lowerValue.includes('data:'))
|
|
169
|
+
return true;
|
|
170
|
+
// CSS-based attack patterns
|
|
171
|
+
if (lowerValue.includes('expression('))
|
|
172
|
+
return true; // IE CSS expressions
|
|
173
|
+
if (lowerValue.includes('@import'))
|
|
174
|
+
return true; // CSS imports
|
|
175
|
+
if (lowerValue.includes('-moz-binding'))
|
|
176
|
+
return true; // Firefox XBL binding
|
|
177
|
+
return false;
|
|
178
|
+
};
|
|
37
179
|
class FloatingButton {
|
|
38
180
|
constructor(options = {}) {
|
|
39
181
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -77,10 +219,12 @@ class FloatingButton {
|
|
|
77
219
|
const btn = document.createElement('button');
|
|
78
220
|
// Set button content (SVG or text)
|
|
79
221
|
if (this.options.customSvg) {
|
|
80
|
-
|
|
222
|
+
// Safely inject custom SVG by parsing and validating it
|
|
223
|
+
this.setSafeHTMLContent(btn, this.options.customSvg);
|
|
81
224
|
}
|
|
82
225
|
else if (this.options.icon === 'svg') {
|
|
83
|
-
|
|
226
|
+
// Safely inject default SVG
|
|
227
|
+
this.setSafeHTMLContent(btn, DEFAULT_SVG_ICON);
|
|
84
228
|
}
|
|
85
229
|
else {
|
|
86
230
|
btn.textContent = this.options.icon;
|
|
@@ -92,6 +236,103 @@ class FloatingButton {
|
|
|
92
236
|
this.addHoverEffects(btn);
|
|
93
237
|
return btn;
|
|
94
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Safely inject HTML content by parsing and validating SVG elements
|
|
241
|
+
* Prevents XSS attacks by only allowing safe SVG elements and attributes
|
|
242
|
+
*/
|
|
243
|
+
setSafeHTMLContent(element, htmlContent) {
|
|
244
|
+
try {
|
|
245
|
+
if (typeof window === 'undefined' ||
|
|
246
|
+
typeof window.DOMParser === 'undefined') {
|
|
247
|
+
element.textContent = htmlContent;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// SECURITY: Use DOMParser with image/svg+xml MIME type for strict SVG parsing
|
|
251
|
+
// This prevents HTML-specific parsing quirks from being exploited
|
|
252
|
+
const parser = new window.DOMParser();
|
|
253
|
+
const doc = parser.parseFromString(htmlContent, 'image/svg+xml');
|
|
254
|
+
// Check for parse errors
|
|
255
|
+
const parserError = doc.querySelector('parsererror');
|
|
256
|
+
if (parserError) {
|
|
257
|
+
element.textContent = htmlContent;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (doc.documentElement &&
|
|
261
|
+
doc.documentElement.nodeType === Node.ELEMENT_NODE) {
|
|
262
|
+
const rootElement = doc.documentElement;
|
|
263
|
+
// SECURITY: Root element MUST be SVG - prevents wrapper element injection
|
|
264
|
+
// Reject structures like <div><svg>...</svg></div>
|
|
265
|
+
if (rootElement.tagName.toLowerCase() === 'svg') {
|
|
266
|
+
// SECURITY: Only proceed if there's exactly one root element
|
|
267
|
+
// This prevents attacks like: <svg></svg><script>alert('XSS')</script>
|
|
268
|
+
if (doc.children.length === 1) {
|
|
269
|
+
// Remove potentially dangerous attributes and event handlers
|
|
270
|
+
this.sanitizeSVGElement(rootElement);
|
|
271
|
+
// Clear the target element and append only the validated SVG element
|
|
272
|
+
element.innerHTML = '';
|
|
273
|
+
element.appendChild(rootElement);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// If not valid SVG, fall back to text content to prevent XSS
|
|
279
|
+
element.textContent = htmlContent;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
// On any error, use text content for safety
|
|
283
|
+
// eslint-disable-next-line no-console
|
|
284
|
+
console.warn('[BugSpotter] Failed to inject custom SVG content:', error);
|
|
285
|
+
element.textContent = htmlContent;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Recursively sanitize SVG elements by removing dangerous tags and attributes
|
|
290
|
+
* Uses whitelists to ensure only safe SVG content is preserved
|
|
291
|
+
*/
|
|
292
|
+
sanitizeSVGElement(element) {
|
|
293
|
+
// Process all elements in the tree
|
|
294
|
+
const elementsToProcess = [element];
|
|
295
|
+
const processedElements = new WeakSet();
|
|
296
|
+
while (elementsToProcess.length > 0) {
|
|
297
|
+
const current = elementsToProcess.pop();
|
|
298
|
+
if (!current || processedElements.has(current))
|
|
299
|
+
continue;
|
|
300
|
+
processedElements.add(current);
|
|
301
|
+
// SECURITY: First, sanitize the current element's attributes (including root)
|
|
302
|
+
// This prevents attacks like <svg onload="alert('XSS')">
|
|
303
|
+
Array.from(current.attributes || []).forEach((attr) => {
|
|
304
|
+
const attrName = attr.name.toLowerCase();
|
|
305
|
+
// SECURITY: Explicitly reject all event handler attributes (on*)
|
|
306
|
+
// This provides defense-in-depth and prevents accidental whitelisting
|
|
307
|
+
if (attrName.startsWith('on')) {
|
|
308
|
+
current.removeAttribute(attr.name);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Only keep whitelisted attributes
|
|
312
|
+
if (!SAFE_SVG_ATTRIBUTES.has(attrName)) {
|
|
313
|
+
current.removeAttribute(attr.name);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Check attribute values for dangerous patterns
|
|
317
|
+
if (isDangerousAttributeValue(attr.value)) {
|
|
318
|
+
current.removeAttribute(attr.name);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// Then, process children elements
|
|
323
|
+
const children = Array.from(current.children || []);
|
|
324
|
+
children.forEach((child) => {
|
|
325
|
+
const tagName = child.tagName.toLowerCase();
|
|
326
|
+
// SECURITY: Remove tags not in whitelist (blocks <script>, <style>, <iframe>, etc.)
|
|
327
|
+
if (!SAFE_SVG_TAGS.has(tagName)) {
|
|
328
|
+
child.remove();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// Add to processing queue for recursive sanitization
|
|
332
|
+
elementsToProcess.push(child);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
95
336
|
getButtonStyles() {
|
|
96
337
|
const { position, size, offset, backgroundColor, zIndex } = this.options;
|
|
97
338
|
const positionStyles = this.getPositionStyles(position, offset);
|
|
@@ -156,7 +397,7 @@ class FloatingButton {
|
|
|
156
397
|
setIcon(icon) {
|
|
157
398
|
this.options.icon = icon;
|
|
158
399
|
if (icon === 'svg') {
|
|
159
|
-
this.button
|
|
400
|
+
this.setSafeHTMLContent(this.button, DEFAULT_SVG_ICON);
|
|
160
401
|
}
|
|
161
402
|
else {
|
|
162
403
|
this.button.textContent = icon;
|
|
@@ -1510,158 +1751,168 @@ class ScreenshotProcessor {
|
|
|
1510
1751
|
/**
|
|
1511
1752
|
* Merge redaction canvas with original screenshot
|
|
1512
1753
|
*/
|
|
1513
|
-
|
|
1514
|
-
return
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1754
|
+
mergeRedactions(originalDataUrl, redactionCanvas) {
|
|
1755
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
1756
|
+
return new Promise((resolve, reject) => {
|
|
1757
|
+
const img = new Image();
|
|
1758
|
+
img.onload = () => {
|
|
1759
|
+
try {
|
|
1760
|
+
const mergedCanvas = document.createElement('canvas');
|
|
1761
|
+
mergedCanvas.width = img.naturalWidth || img.width;
|
|
1762
|
+
mergedCanvas.height = img.naturalHeight || img.height;
|
|
1763
|
+
const ctx = mergedCanvas.getContext('2d');
|
|
1764
|
+
if (!ctx) {
|
|
1765
|
+
reject(new Error('Failed to get canvas context'));
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
// Draw original image
|
|
1769
|
+
ctx.drawImage(img, 0, 0);
|
|
1770
|
+
// Draw redaction canvas on top
|
|
1771
|
+
ctx.drawImage(redactionCanvas, 0, 0);
|
|
1772
|
+
resolve(mergedCanvas.toDataURL());
|
|
1525
1773
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
};
|
|
1536
|
-
img.onerror = () => {
|
|
1537
|
-
reject(new Error('Failed to load screenshot image'));
|
|
1538
|
-
};
|
|
1539
|
-
img.src = originalDataUrl;
|
|
1774
|
+
catch (error) {
|
|
1775
|
+
reject(error);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
img.onerror = () => {
|
|
1779
|
+
reject(new Error('Failed to load screenshot image'));
|
|
1780
|
+
};
|
|
1781
|
+
img.src = originalDataUrl;
|
|
1782
|
+
});
|
|
1540
1783
|
});
|
|
1541
1784
|
}
|
|
1542
1785
|
/**
|
|
1543
1786
|
* Apply redaction rectangles directly to an image data URL
|
|
1544
1787
|
*/
|
|
1545
|
-
|
|
1546
|
-
return
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1788
|
+
applyRedactions(imageDataUrl_1, redactions_1) {
|
|
1789
|
+
return __awaiter$1(this, arguments, void 0, function* (imageDataUrl, redactions, redactionColor = '#000000') {
|
|
1790
|
+
return new Promise((resolve, reject) => {
|
|
1791
|
+
const img = new Image();
|
|
1792
|
+
img.onload = () => {
|
|
1793
|
+
try {
|
|
1794
|
+
const canvas = document.createElement('canvas');
|
|
1795
|
+
canvas.width = img.naturalWidth || img.width;
|
|
1796
|
+
canvas.height = img.naturalHeight || img.height;
|
|
1797
|
+
const ctx = canvas.getContext('2d');
|
|
1798
|
+
if (!ctx) {
|
|
1799
|
+
reject(new Error('Failed to get canvas context'));
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
// Draw original image
|
|
1803
|
+
ctx.drawImage(img, 0, 0);
|
|
1804
|
+
// Apply redactions
|
|
1805
|
+
ctx.fillStyle = redactionColor;
|
|
1806
|
+
for (const rect of redactions) {
|
|
1807
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1808
|
+
}
|
|
1809
|
+
resolve(canvas.toDataURL());
|
|
1557
1810
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
// Apply redactions
|
|
1561
|
-
ctx.fillStyle = redactionColor;
|
|
1562
|
-
for (const rect of redactions) {
|
|
1563
|
-
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
1811
|
+
catch (error) {
|
|
1812
|
+
reject(error);
|
|
1564
1813
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
};
|
|
1571
|
-
img.onerror = () => {
|
|
1572
|
-
reject(new Error('Failed to load image'));
|
|
1573
|
-
};
|
|
1574
|
-
img.src = imageDataUrl;
|
|
1814
|
+
};
|
|
1815
|
+
img.onerror = () => {
|
|
1816
|
+
reject(new Error('Failed to load image'));
|
|
1817
|
+
};
|
|
1818
|
+
img.src = imageDataUrl;
|
|
1819
|
+
});
|
|
1575
1820
|
});
|
|
1576
1821
|
}
|
|
1577
1822
|
/**
|
|
1578
1823
|
* Resize an image to maximum dimensions while maintaining aspect ratio
|
|
1579
1824
|
*/
|
|
1580
|
-
|
|
1581
|
-
return
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
width
|
|
1592
|
-
|
|
1825
|
+
resize(imageDataUrl, maxWidth, maxHeight) {
|
|
1826
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
1827
|
+
return new Promise((resolve, reject) => {
|
|
1828
|
+
const img = new Image();
|
|
1829
|
+
img.onload = () => {
|
|
1830
|
+
try {
|
|
1831
|
+
let width = img.naturalWidth || img.width;
|
|
1832
|
+
let height = img.naturalHeight || img.height;
|
|
1833
|
+
// Calculate new dimensions maintaining aspect ratio
|
|
1834
|
+
if (width > maxWidth || height > maxHeight) {
|
|
1835
|
+
const aspectRatio = width / height;
|
|
1836
|
+
if (width > height) {
|
|
1837
|
+
width = maxWidth;
|
|
1838
|
+
height = width / aspectRatio;
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
height = maxHeight;
|
|
1842
|
+
width = height * aspectRatio;
|
|
1843
|
+
}
|
|
1593
1844
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1845
|
+
const canvas = document.createElement('canvas');
|
|
1846
|
+
canvas.width = width;
|
|
1847
|
+
canvas.height = height;
|
|
1848
|
+
const ctx = canvas.getContext('2d');
|
|
1849
|
+
if (!ctx) {
|
|
1850
|
+
reject(new Error('Failed to get canvas context'));
|
|
1851
|
+
return;
|
|
1597
1852
|
}
|
|
1853
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1854
|
+
resolve(canvas.toDataURL());
|
|
1598
1855
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
canvas.height = height;
|
|
1602
|
-
const ctx = canvas.getContext('2d');
|
|
1603
|
-
if (!ctx) {
|
|
1604
|
-
reject(new Error('Failed to get canvas context'));
|
|
1605
|
-
return;
|
|
1856
|
+
catch (error) {
|
|
1857
|
+
reject(error);
|
|
1606
1858
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
};
|
|
1614
|
-
img.onerror = () => {
|
|
1615
|
-
reject(new Error('Failed to load image'));
|
|
1616
|
-
};
|
|
1617
|
-
img.src = imageDataUrl;
|
|
1859
|
+
};
|
|
1860
|
+
img.onerror = () => {
|
|
1861
|
+
reject(new Error('Failed to load image'));
|
|
1862
|
+
};
|
|
1863
|
+
img.src = imageDataUrl;
|
|
1864
|
+
});
|
|
1618
1865
|
});
|
|
1619
1866
|
}
|
|
1620
1867
|
/**
|
|
1621
1868
|
* Convert image to different format
|
|
1622
1869
|
*/
|
|
1623
|
-
|
|
1624
|
-
return
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1870
|
+
convert(imageDataUrl_1, format_1) {
|
|
1871
|
+
return __awaiter$1(this, arguments, void 0, function* (imageDataUrl, format, quality = 0.92) {
|
|
1872
|
+
return new Promise((resolve, reject) => {
|
|
1873
|
+
const img = new Image();
|
|
1874
|
+
img.onload = () => {
|
|
1875
|
+
try {
|
|
1876
|
+
const canvas = document.createElement('canvas');
|
|
1877
|
+
canvas.width = img.naturalWidth || img.width;
|
|
1878
|
+
canvas.height = img.naturalHeight || img.height;
|
|
1879
|
+
const ctx = canvas.getContext('2d');
|
|
1880
|
+
if (!ctx) {
|
|
1881
|
+
reject(new Error('Failed to get canvas context'));
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
ctx.drawImage(img, 0, 0);
|
|
1885
|
+
resolve(canvas.toDataURL(format, quality));
|
|
1635
1886
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
};
|
|
1646
|
-
img.src = imageDataUrl;
|
|
1887
|
+
catch (error) {
|
|
1888
|
+
reject(error);
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
img.onerror = () => {
|
|
1892
|
+
reject(new Error('Failed to load image'));
|
|
1893
|
+
};
|
|
1894
|
+
img.src = imageDataUrl;
|
|
1895
|
+
});
|
|
1647
1896
|
});
|
|
1648
1897
|
}
|
|
1649
1898
|
/**
|
|
1650
1899
|
* Get image dimensions from data URL
|
|
1651
1900
|
*/
|
|
1652
|
-
|
|
1653
|
-
return
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1901
|
+
getDimensions(imageDataUrl) {
|
|
1902
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
1903
|
+
return new Promise((resolve, reject) => {
|
|
1904
|
+
const img = new Image();
|
|
1905
|
+
img.onload = () => {
|
|
1906
|
+
resolve({
|
|
1907
|
+
width: img.naturalWidth || img.width,
|
|
1908
|
+
height: img.naturalHeight || img.height,
|
|
1909
|
+
});
|
|
1910
|
+
};
|
|
1911
|
+
img.onerror = () => {
|
|
1912
|
+
reject(new Error('Failed to load image'));
|
|
1913
|
+
};
|
|
1914
|
+
img.src = imageDataUrl;
|
|
1915
|
+
});
|
|
1665
1916
|
});
|
|
1666
1917
|
}
|
|
1667
1918
|
/**
|
|
@@ -1689,106 +1940,124 @@ class ScreenshotProcessor {
|
|
|
1689
1940
|
}
|
|
1690
1941
|
|
|
1691
1942
|
/**
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1943
|
+
* Generic circular buffer — fixed-size FIFO that overwrites oldest items.
|
|
1944
|
+
*
|
|
1945
|
+
* @template T The type of items stored in the buffer
|
|
1694
1946
|
*/
|
|
1947
|
+
let CircularBuffer$1 = class CircularBuffer {
|
|
1948
|
+
constructor(maxSize) {
|
|
1949
|
+
this.maxSize = maxSize;
|
|
1950
|
+
this.items = [];
|
|
1951
|
+
this.index = 0;
|
|
1952
|
+
this.count = 0;
|
|
1953
|
+
if (maxSize <= 0) {
|
|
1954
|
+
throw new Error('CircularBuffer maxSize must be greater than 0');
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Add an item to the buffer. If full, the oldest item is overwritten.
|
|
1959
|
+
*/
|
|
1960
|
+
add(item) {
|
|
1961
|
+
if (this.count < this.maxSize) {
|
|
1962
|
+
this.items.push(item);
|
|
1963
|
+
this.count++;
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
this.items[this.index] = item;
|
|
1967
|
+
}
|
|
1968
|
+
this.index = (this.index + 1) % this.maxSize;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Get all items in chronological order (oldest to newest).
|
|
1972
|
+
* Returns a copy of the internal array.
|
|
1973
|
+
*/
|
|
1974
|
+
getAll() {
|
|
1975
|
+
if (this.count < this.maxSize) {
|
|
1976
|
+
return [...this.items];
|
|
1977
|
+
}
|
|
1978
|
+
return [
|
|
1979
|
+
...this.items.slice(this.index),
|
|
1980
|
+
...this.items.slice(0, this.index),
|
|
1981
|
+
];
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Clear all items from the buffer.
|
|
1985
|
+
*/
|
|
1986
|
+
clear() {
|
|
1987
|
+
this.items = [];
|
|
1988
|
+
this.index = 0;
|
|
1989
|
+
this.count = 0;
|
|
1990
|
+
}
|
|
1991
|
+
get size() {
|
|
1992
|
+
return this.count;
|
|
1993
|
+
}
|
|
1994
|
+
get capacity() {
|
|
1995
|
+
return this.maxSize;
|
|
1996
|
+
}
|
|
1997
|
+
get isEmpty() {
|
|
1998
|
+
return this.count === 0;
|
|
1999
|
+
}
|
|
2000
|
+
get isFull() {
|
|
2001
|
+
return this.count >= this.maxSize;
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
|
|
1695
2005
|
/**
|
|
1696
|
-
*
|
|
2006
|
+
* PII pattern definitions for data sanitization.
|
|
1697
2007
|
*/
|
|
1698
2008
|
const DEFAULT_PATTERNS = {
|
|
1699
2009
|
email: {
|
|
1700
2010
|
name: 'email',
|
|
1701
2011
|
regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
1702
2012
|
description: 'Email addresses',
|
|
1703
|
-
|
|
1704
|
-
'user@example.com',
|
|
1705
|
-
'john.doe+tag@company.co.uk',
|
|
1706
|
-
'test_user@sub.domain.com',
|
|
1707
|
-
],
|
|
1708
|
-
priority: 1, // Highest priority - most specific
|
|
2013
|
+
priority: 1,
|
|
1709
2014
|
},
|
|
1710
2015
|
creditcard: {
|
|
1711
2016
|
name: 'creditcard',
|
|
1712
2017
|
regex: /\b(?:\d{4}[-\s]){3}\d{4}\b|\b\d{4}[-\s]\d{6}[-\s]\d{5}\b|\b\d{13,19}\b/g,
|
|
1713
|
-
description: 'Credit card numbers
|
|
1714
|
-
examples: [
|
|
1715
|
-
'4532-1488-0343-6467',
|
|
1716
|
-
'4532148803436467',
|
|
1717
|
-
'5425 2334 3010 9903',
|
|
1718
|
-
'3782 822463 10005',
|
|
1719
|
-
],
|
|
2018
|
+
description: 'Credit card numbers',
|
|
1720
2019
|
priority: 2,
|
|
1721
2020
|
},
|
|
1722
2021
|
ssn: {
|
|
1723
2022
|
name: 'ssn',
|
|
1724
2023
|
regex: /\b\d{3}-\d{2}-\d{4}\b|\b(?<!\d)\d{9}(?!\d)\b/g,
|
|
1725
2024
|
description: 'US Social Security Numbers',
|
|
1726
|
-
examples: ['123-45-6789', '987654321'],
|
|
1727
2025
|
priority: 3,
|
|
1728
2026
|
},
|
|
1729
2027
|
iin: {
|
|
1730
2028
|
name: 'iin',
|
|
1731
2029
|
regex: /\b[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01])\d{6}\b/g,
|
|
1732
|
-
description: 'Kazakhstan IIN/BIN
|
|
1733
|
-
examples: ['950315300123', '880612500456', '021225123456'],
|
|
2030
|
+
description: 'Kazakhstan IIN/BIN',
|
|
1734
2031
|
priority: 4,
|
|
1735
2032
|
},
|
|
1736
2033
|
ip: {
|
|
1737
2034
|
name: 'ip',
|
|
1738
2035
|
regex: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g,
|
|
1739
2036
|
description: 'IPv4 and IPv6 addresses',
|
|
1740
|
-
examples: [
|
|
1741
|
-
'192.168.1.100',
|
|
1742
|
-
'127.0.0.1',
|
|
1743
|
-
'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
|
1744
|
-
],
|
|
1745
2037
|
priority: 5,
|
|
1746
2038
|
},
|
|
1747
2039
|
phone: {
|
|
1748
2040
|
name: 'phone',
|
|
1749
2041
|
regex: /\+\d{1,3}[-.\s]\d{3}[-.\s]\d{4}\b|\+\d{1,3}[-.\s]\d{3}[-.\s]\d{3}[-.\s]\d{4}\b|\(\d{3}\)\s*\d{3}[-.\s]\d{4}\b|\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b/g,
|
|
1750
|
-
description: '
|
|
1751
|
-
examples: [
|
|
1752
|
-
'+1-555-1234',
|
|
1753
|
-
'+1-555-123-4567',
|
|
1754
|
-
'(555) 123-4567',
|
|
1755
|
-
'555-123-4567',
|
|
1756
|
-
'+7 777 123 4567',
|
|
1757
|
-
],
|
|
2042
|
+
description: 'Phone numbers',
|
|
1758
2043
|
priority: 6,
|
|
1759
2044
|
},
|
|
1760
2045
|
apikey: {
|
|
1761
2046
|
name: 'apikey',
|
|
1762
2047
|
regex: /\b(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24,}\b|AIza[a-zA-Z0-9_-]{35}|ya29\.[a-zA-Z0-9_-]+|AKIA[a-zA-Z0-9]{16}\b/g,
|
|
1763
|
-
description: 'API keys (Stripe, Google, AWS
|
|
1764
|
-
examples: [
|
|
1765
|
-
'sk_live_abc123def456ghi789jkl',
|
|
1766
|
-
'pk_test_xyz789abc123def456',
|
|
1767
|
-
'AIzaSyD1234567890abcdefghijklmnopqrst',
|
|
1768
|
-
'AKIAIOSFODNN7EXAMPLE',
|
|
1769
|
-
],
|
|
2048
|
+
description: 'API keys (Stripe, Google, AWS)',
|
|
1770
2049
|
priority: 7,
|
|
1771
2050
|
},
|
|
1772
2051
|
token: {
|
|
1773
2052
|
name: 'token',
|
|
1774
2053
|
regex: /\b(?:Bearer\s+)?[a-zA-Z0-9_-]{32,}\b|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9_]{82}/g,
|
|
1775
|
-
description: '
|
|
1776
|
-
examples: [
|
|
1777
|
-
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
|
1778
|
-
'ghp_abc123def456ghi789jkl012mno345pqr',
|
|
1779
|
-
'gho_xyz789abc123def456ghi789jkl012mno',
|
|
1780
|
-
],
|
|
2054
|
+
description: 'Auth tokens (Bearer, GitHub, JWT-like)',
|
|
1781
2055
|
priority: 8,
|
|
1782
2056
|
},
|
|
1783
2057
|
password: {
|
|
1784
2058
|
name: 'password',
|
|
1785
2059
|
regex: /(?:password|passwd|pwd)[\s:=]+[^\s]{6,}|(?:password|passwd|pwd)["']?\s*[:=]\s*["']?[^\s"']{6,}/gi,
|
|
1786
|
-
description: 'Password fields in text
|
|
1787
|
-
examples: [
|
|
1788
|
-
'password: MySecret123!',
|
|
1789
|
-
'passwd=SecurePass456',
|
|
1790
|
-
'pwd: "MyP@ssw0rd"',
|
|
1791
|
-
],
|
|
2060
|
+
description: 'Password fields in text',
|
|
1792
2061
|
priority: 9,
|
|
1793
2062
|
},
|
|
1794
2063
|
};
|
|
@@ -1804,32 +2073,160 @@ const PATTERN_CATEGORIES = {
|
|
|
1804
2073
|
kazakhstan: ['iin'],
|
|
1805
2074
|
};
|
|
1806
2075
|
/**
|
|
1807
|
-
*
|
|
2076
|
+
* Pre-configured pattern sets for common compliance/use cases
|
|
1808
2077
|
*/
|
|
2078
|
+
const PATTERN_PRESETS = {
|
|
2079
|
+
all: Object.keys(DEFAULT_PATTERNS),
|
|
2080
|
+
minimal: ['email', 'creditcard', 'ssn'],
|
|
2081
|
+
financial: ['creditcard', 'ssn'],
|
|
2082
|
+
contact: ['email', 'phone'],
|
|
2083
|
+
identification: ['ssn', 'iin'],
|
|
2084
|
+
credentials: ['apikey', 'token', 'password'],
|
|
2085
|
+
kazakhstan: ['email', 'phone', 'iin'],
|
|
2086
|
+
gdpr: ['email', 'phone', 'ip'],
|
|
2087
|
+
pci: ['creditcard'],
|
|
2088
|
+
security: [
|
|
2089
|
+
'email',
|
|
2090
|
+
'phone',
|
|
2091
|
+
'creditcard',
|
|
2092
|
+
'ssn',
|
|
2093
|
+
'apikey',
|
|
2094
|
+
'token',
|
|
2095
|
+
'password',
|
|
2096
|
+
],
|
|
2097
|
+
};
|
|
1809
2098
|
function getPatternsByPriority(patterns) {
|
|
1810
|
-
return [...patterns].sort((a, b) =>
|
|
1811
|
-
|
|
1812
|
-
|
|
2099
|
+
return [...patterns].sort((a, b) => a.priority - b.priority);
|
|
2100
|
+
}
|
|
2101
|
+
function createPatternConfig(preset) {
|
|
2102
|
+
const names = typeof preset === 'string' ? PATTERN_PRESETS[preset] : preset;
|
|
2103
|
+
return names.map((name) => DEFAULT_PATTERNS[name]);
|
|
1813
2104
|
}
|
|
1814
2105
|
/**
|
|
1815
|
-
*
|
|
2106
|
+
* Validate a custom pattern regex for performance issues
|
|
1816
2107
|
*/
|
|
1817
|
-
function
|
|
1818
|
-
|
|
2108
|
+
function validatePattern(pattern) {
|
|
2109
|
+
const errors = [];
|
|
2110
|
+
if (!pattern.name)
|
|
2111
|
+
errors.push('Pattern must have a name');
|
|
2112
|
+
if (!pattern.regex) {
|
|
2113
|
+
errors.push('Pattern must have a regex');
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
if (!pattern.regex.global)
|
|
2117
|
+
errors.push('Pattern regex must have global flag');
|
|
2118
|
+
try {
|
|
2119
|
+
const testString = 'a'.repeat(1000);
|
|
2120
|
+
const start = Date.now();
|
|
2121
|
+
testString.match(pattern.regex);
|
|
2122
|
+
if (Date.now() - start > 100) {
|
|
2123
|
+
errors.push('Pattern regex may cause performance issues');
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
catch (e) {
|
|
2127
|
+
errors.push(`Pattern regex error: ${e.message}`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return { valid: errors.length === 0, errors };
|
|
1819
2131
|
}
|
|
2132
|
+
|
|
1820
2133
|
/**
|
|
1821
|
-
*
|
|
2134
|
+
* URL helper utilities for endpoint validation and parsing.
|
|
1822
2135
|
*/
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
2136
|
+
/**
|
|
2137
|
+
* Custom error for invalid endpoint URLs
|
|
2138
|
+
*/
|
|
2139
|
+
class InvalidEndpointError extends Error {
|
|
2140
|
+
constructor(endpoint, reason) {
|
|
2141
|
+
super(`Invalid endpoint URL: ${endpoint}. ${reason}`);
|
|
2142
|
+
this.endpoint = endpoint;
|
|
2143
|
+
this.reason = reason;
|
|
2144
|
+
this.name = 'InvalidEndpointError';
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Custom error for insecure endpoints
|
|
2149
|
+
*/
|
|
2150
|
+
class InsecureEndpointError extends Error {
|
|
2151
|
+
constructor(endpoint) {
|
|
2152
|
+
super(`Secure HTTPS connection required. Attempted to connect to insecure endpoint: "${endpoint}"`);
|
|
2153
|
+
this.endpoint = endpoint;
|
|
2154
|
+
this.name = 'InsecureEndpointError';
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Strip known endpoint suffixes from path.
|
|
2159
|
+
* Removes /api/v1/reports path.
|
|
2160
|
+
*/
|
|
2161
|
+
function stripEndpointSuffix(path) {
|
|
2162
|
+
const reportsIndex = path.lastIndexOf('/api/v1/reports');
|
|
2163
|
+
if (reportsIndex !== -1) {
|
|
2164
|
+
return path.substring(0, reportsIndex);
|
|
2165
|
+
}
|
|
2166
|
+
return path.replace(/\/$/, '') || '';
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Extract base API URL from endpoint.
|
|
2170
|
+
* Returns scheme + host + base path (without /api/v1/reports suffix).
|
|
2171
|
+
*
|
|
2172
|
+
* @example
|
|
2173
|
+
* getApiBaseUrl('https://api.example.com/api/v1/reports')
|
|
2174
|
+
* // Returns: 'https://api.example.com'
|
|
2175
|
+
*
|
|
2176
|
+
* @throws InvalidEndpointError if endpoint is not a valid absolute URL
|
|
2177
|
+
*/
|
|
2178
|
+
function getApiBaseUrl(endpoint) {
|
|
2179
|
+
if (!endpoint) {
|
|
2180
|
+
throw new InvalidEndpointError('', 'No endpoint configured');
|
|
2181
|
+
}
|
|
2182
|
+
try {
|
|
2183
|
+
const url = new URL(endpoint);
|
|
2184
|
+
const basePath = stripEndpointSuffix(url.pathname);
|
|
2185
|
+
return url.origin + basePath;
|
|
2186
|
+
}
|
|
2187
|
+
catch {
|
|
2188
|
+
throw new InvalidEndpointError(endpoint, 'Must be a valid absolute URL (e.g., https://api.example.com/api/v1/reports)');
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Checks if the endpoint uses a secure protocol.
|
|
2193
|
+
* Uses the URL API for robust parsing.
|
|
2194
|
+
*
|
|
2195
|
+
* Allows HTTPS in production, HTTP only on localhost/127.0.0.1 for development.
|
|
2196
|
+
*/
|
|
2197
|
+
function isSecureEndpoint(endpoint) {
|
|
2198
|
+
if (!endpoint)
|
|
2199
|
+
return false;
|
|
2200
|
+
try {
|
|
2201
|
+
const url = new URL(endpoint.trim());
|
|
2202
|
+
return (url.protocol === 'https:' ||
|
|
2203
|
+
(url.protocol === 'http:' &&
|
|
2204
|
+
(url.hostname === 'localhost' || url.hostname === '127.0.0.1')));
|
|
2205
|
+
}
|
|
2206
|
+
catch {
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* PII Pattern Definitions
|
|
2213
|
+
* Re-exports from @bugspotter/common + SDK-specific extensions
|
|
2214
|
+
*/
|
|
2215
|
+
// Re-export everything from @bugspotter/common
|
|
2216
|
+
// SDK-specific extensions
|
|
2217
|
+
/**
|
|
2218
|
+
* Get pattern by name
|
|
2219
|
+
*/
|
|
2220
|
+
function getPattern(name) {
|
|
2221
|
+
return DEFAULT_PATTERNS[name];
|
|
1827
2222
|
}
|
|
1828
2223
|
/**
|
|
1829
|
-
* Get
|
|
2224
|
+
* Get patterns by category
|
|
1830
2225
|
*/
|
|
1831
|
-
function
|
|
1832
|
-
return
|
|
2226
|
+
function getPatternsByCategory(category) {
|
|
2227
|
+
return PATTERN_CATEGORIES[category].map((name) => {
|
|
2228
|
+
return DEFAULT_PATTERNS[name];
|
|
2229
|
+
});
|
|
1833
2230
|
}
|
|
1834
2231
|
/**
|
|
1835
2232
|
* Custom pattern builder for advanced use cases
|
|
@@ -1843,7 +2240,6 @@ class PatternBuilder {
|
|
|
1843
2240
|
return this;
|
|
1844
2241
|
}
|
|
1845
2242
|
regex(regex) {
|
|
1846
|
-
// Ensure global flag
|
|
1847
2243
|
if (!regex.global) {
|
|
1848
2244
|
const flags = regex.flags.includes('g') ? regex.flags : regex.flags + 'g';
|
|
1849
2245
|
this.pattern.regex = new RegExp(regex.source, flags);
|
|
@@ -1879,85 +2275,6 @@ class PatternBuilder {
|
|
|
1879
2275
|
};
|
|
1880
2276
|
}
|
|
1881
2277
|
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Pre-configured pattern sets for common use cases
|
|
1884
|
-
*/
|
|
1885
|
-
const PATTERN_PRESETS = {
|
|
1886
|
-
/** All patterns enabled (PII + credentials) - default */
|
|
1887
|
-
all: getAllPatternNames(),
|
|
1888
|
-
/** Minimal - only most critical PII */
|
|
1889
|
-
minimal: ['email', 'creditcard', 'ssn'],
|
|
1890
|
-
/** Financial data only */
|
|
1891
|
-
financial: PATTERN_CATEGORIES.financial,
|
|
1892
|
-
/** Contact information only */
|
|
1893
|
-
contact: PATTERN_CATEGORIES.contact,
|
|
1894
|
-
/** Identification numbers only */
|
|
1895
|
-
identification: PATTERN_CATEGORIES.identification,
|
|
1896
|
-
/** Credentials and secrets only */
|
|
1897
|
-
credentials: PATTERN_CATEGORIES.credentials,
|
|
1898
|
-
/** Kazakhstan-specific patterns */
|
|
1899
|
-
kazakhstan: ['email', 'phone', 'iin'],
|
|
1900
|
-
/** GDPR compliance recommended set */
|
|
1901
|
-
gdpr: ['email', 'phone', 'ip'],
|
|
1902
|
-
/** PCI DSS compliance required */
|
|
1903
|
-
pci: ['creditcard'],
|
|
1904
|
-
/** Security-focused: PII + credentials */
|
|
1905
|
-
security: [
|
|
1906
|
-
'email',
|
|
1907
|
-
'phone',
|
|
1908
|
-
'creditcard',
|
|
1909
|
-
'ssn',
|
|
1910
|
-
'apikey',
|
|
1911
|
-
'token',
|
|
1912
|
-
'password',
|
|
1913
|
-
],
|
|
1914
|
-
};
|
|
1915
|
-
/**
|
|
1916
|
-
* Create custom pattern configuration
|
|
1917
|
-
*/
|
|
1918
|
-
function createPatternConfig(preset) {
|
|
1919
|
-
const names = typeof preset === 'string' ? PATTERN_PRESETS[preset] : preset;
|
|
1920
|
-
return names.map((name) => {
|
|
1921
|
-
return DEFAULT_PATTERNS[name];
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
/**
|
|
1925
|
-
* Validate pattern regex
|
|
1926
|
-
*/
|
|
1927
|
-
function validatePattern(pattern) {
|
|
1928
|
-
const errors = [];
|
|
1929
|
-
if (!pattern.name) {
|
|
1930
|
-
errors.push('Pattern must have a name');
|
|
1931
|
-
}
|
|
1932
|
-
if (!pattern.regex) {
|
|
1933
|
-
errors.push('Pattern must have a regex');
|
|
1934
|
-
}
|
|
1935
|
-
else {
|
|
1936
|
-
if (!pattern.regex.global) {
|
|
1937
|
-
errors.push('Pattern regex must have global flag');
|
|
1938
|
-
}
|
|
1939
|
-
// Test regex doesn't cause catastrophic backtracking
|
|
1940
|
-
try {
|
|
1941
|
-
const testString = 'a'.repeat(1000);
|
|
1942
|
-
const start = Date.now();
|
|
1943
|
-
testString.match(pattern.regex);
|
|
1944
|
-
const duration = Date.now() - start;
|
|
1945
|
-
if (duration > 100) {
|
|
1946
|
-
errors.push(`Pattern regex may cause performance issues (took ${duration}ms on test)`);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
catch (error) {
|
|
1950
|
-
errors.push(`Pattern regex error: ${error.message}`);
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
if (pattern.priority < 0) {
|
|
1954
|
-
errors.push('Pattern priority must be non-negative');
|
|
1955
|
-
}
|
|
1956
|
-
return {
|
|
1957
|
-
valid: errors.length === 0,
|
|
1958
|
-
errors,
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
2278
|
|
|
1962
2279
|
/**
|
|
1963
2280
|
* PII Detection and Sanitization Utility - REFACTORED
|
|
@@ -2279,7 +2596,7 @@ function createLogger(config) {
|
|
|
2279
2596
|
return new BugSpotterLogger(config);
|
|
2280
2597
|
}
|
|
2281
2598
|
|
|
2282
|
-
const logger$
|
|
2599
|
+
const logger$7 = getLogger();
|
|
2283
2600
|
/**
|
|
2284
2601
|
* BugReportModal
|
|
2285
2602
|
*
|
|
@@ -2494,99 +2811,101 @@ class BugReportModal {
|
|
|
2494
2811
|
this.redactionCanvas.clearRedactions();
|
|
2495
2812
|
}
|
|
2496
2813
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
const formData = {
|
|
2506
|
-
title: elements.titleInput.value,
|
|
2507
|
-
description: elements.descriptionTextarea.value,
|
|
2508
|
-
piiDetected: this.piiDetections.length > 0,
|
|
2509
|
-
piiConfirmed: elements.piiConfirmCheckbox.checked,
|
|
2510
|
-
};
|
|
2511
|
-
const validation = this.validator.validate(formData);
|
|
2512
|
-
if (!validation.isValid) {
|
|
2513
|
-
// Display errors
|
|
2514
|
-
if (validation.errors.title) {
|
|
2515
|
-
elements.titleError.textContent = validation.errors.title;
|
|
2516
|
-
elements.titleError.style.display = 'block';
|
|
2517
|
-
}
|
|
2518
|
-
else {
|
|
2519
|
-
elements.titleError.textContent = '';
|
|
2520
|
-
elements.titleError.style.display = 'none';
|
|
2521
|
-
}
|
|
2522
|
-
if (validation.errors.description) {
|
|
2523
|
-
elements.descriptionError.textContent = validation.errors.description;
|
|
2524
|
-
elements.descriptionError.style.display = 'block';
|
|
2525
|
-
}
|
|
2526
|
-
else {
|
|
2527
|
-
elements.descriptionError.textContent = '';
|
|
2528
|
-
elements.descriptionError.style.display = 'none';
|
|
2814
|
+
handleSubmit(e) {
|
|
2815
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
2816
|
+
var _a;
|
|
2817
|
+
e.preventDefault();
|
|
2818
|
+
const elements = this.domCache.get();
|
|
2819
|
+
// Prevent double submission
|
|
2820
|
+
if (elements.submitButton.disabled) {
|
|
2821
|
+
return;
|
|
2529
2822
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2823
|
+
const formData = {
|
|
2824
|
+
title: elements.titleInput.value,
|
|
2825
|
+
description: elements.descriptionTextarea.value,
|
|
2826
|
+
piiDetected: this.piiDetections.length > 0,
|
|
2827
|
+
piiConfirmed: elements.piiConfirmCheckbox.checked,
|
|
2828
|
+
};
|
|
2829
|
+
const validation = this.validator.validate(formData);
|
|
2830
|
+
if (!validation.isValid) {
|
|
2831
|
+
// Display errors
|
|
2832
|
+
if (validation.errors.title) {
|
|
2833
|
+
elements.titleError.textContent = validation.errors.title;
|
|
2834
|
+
elements.titleError.style.display = 'block';
|
|
2835
|
+
}
|
|
2836
|
+
else {
|
|
2837
|
+
elements.titleError.textContent = '';
|
|
2838
|
+
elements.titleError.style.display = 'none';
|
|
2839
|
+
}
|
|
2840
|
+
if (validation.errors.description) {
|
|
2841
|
+
elements.descriptionError.textContent = validation.errors.description;
|
|
2842
|
+
elements.descriptionError.style.display = 'block';
|
|
2843
|
+
}
|
|
2844
|
+
else {
|
|
2845
|
+
elements.descriptionError.textContent = '';
|
|
2846
|
+
elements.descriptionError.style.display = 'none';
|
|
2847
|
+
}
|
|
2848
|
+
if (validation.errors.piiConfirmation) {
|
|
2849
|
+
alert(validation.errors.piiConfirmation);
|
|
2850
|
+
}
|
|
2851
|
+
return;
|
|
2532
2852
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2853
|
+
// Clear any previous error messages on successful validation
|
|
2854
|
+
elements.titleError.style.display = 'none';
|
|
2855
|
+
elements.descriptionError.style.display = 'none';
|
|
2856
|
+
elements.submitError.style.display = 'none';
|
|
2857
|
+
// Disable submit button and show loading state
|
|
2858
|
+
const originalButtonText = elements.submitButton.textContent || 'Submit Bug Report';
|
|
2859
|
+
elements.submitButton.disabled = true;
|
|
2860
|
+
elements.submitButton.textContent = 'Preparing...';
|
|
2861
|
+
elements.submitButton.classList.add('loading');
|
|
2862
|
+
// Helper to update progress
|
|
2863
|
+
const updateProgress = (message) => {
|
|
2864
|
+
elements.submitButton.textContent = message;
|
|
2865
|
+
elements.progressStatus.textContent = message; // Announce to screen readers
|
|
2866
|
+
if (this.options.onProgress) {
|
|
2867
|
+
this.options.onProgress(message);
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
updateProgress('Preparing screenshot...');
|
|
2871
|
+
// Prepare screenshot with redactions
|
|
2872
|
+
let finalScreenshot = this.originalScreenshot;
|
|
2873
|
+
if (this.redactionCanvas &&
|
|
2874
|
+
this.redactionCanvas.getRedactions().length > 0) {
|
|
2875
|
+
try {
|
|
2876
|
+
finalScreenshot = yield this.screenshotProcessor.mergeRedactions(this.originalScreenshot, this.redactionCanvas.getCanvas());
|
|
2877
|
+
}
|
|
2878
|
+
catch (mergeError) {
|
|
2879
|
+
logger$7.error('Failed to merge redactions:', mergeError);
|
|
2880
|
+
finalScreenshot = this.originalScreenshot;
|
|
2881
|
+
}
|
|
2550
2882
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2883
|
+
// Update original screenshot for submission
|
|
2884
|
+
this.originalScreenshot = finalScreenshot;
|
|
2885
|
+
// Submit
|
|
2886
|
+
const bugReportData = {
|
|
2887
|
+
title: formData.title.trim(),
|
|
2888
|
+
description: (_a = formData.description) === null || _a === void 0 ? void 0 : _a.trim(),
|
|
2889
|
+
};
|
|
2557
2890
|
try {
|
|
2558
|
-
|
|
2891
|
+
updateProgress('Uploading report...');
|
|
2892
|
+
yield this.options.onSubmit(bugReportData);
|
|
2893
|
+
this.close();
|
|
2559
2894
|
}
|
|
2560
|
-
catch (
|
|
2561
|
-
|
|
2562
|
-
|
|
2895
|
+
catch (error) {
|
|
2896
|
+
getLogger().error('Error submitting bug report:', error);
|
|
2897
|
+
// Show error message in modal instead of blocking alert
|
|
2898
|
+
elements.submitError.textContent =
|
|
2899
|
+
'Failed to submit bug report. Please try again.';
|
|
2900
|
+
elements.submitError.style.display = 'block';
|
|
2901
|
+
// Clear stale progress status for screen readers
|
|
2902
|
+
elements.progressStatus.textContent = '';
|
|
2903
|
+
// Re-enable submit button on error
|
|
2904
|
+
elements.submitButton.disabled = false;
|
|
2905
|
+
elements.submitButton.textContent = originalButtonText;
|
|
2906
|
+
elements.submitButton.classList.remove('loading');
|
|
2563
2907
|
}
|
|
2564
|
-
}
|
|
2565
|
-
// Update original screenshot for submission
|
|
2566
|
-
this.originalScreenshot = finalScreenshot;
|
|
2567
|
-
// Submit
|
|
2568
|
-
const bugReportData = {
|
|
2569
|
-
title: formData.title.trim(),
|
|
2570
|
-
description: (_a = formData.description) === null || _a === void 0 ? void 0 : _a.trim(),
|
|
2571
|
-
};
|
|
2572
|
-
try {
|
|
2573
|
-
updateProgress('Uploading report...');
|
|
2574
|
-
await this.options.onSubmit(bugReportData);
|
|
2575
|
-
this.close();
|
|
2576
|
-
}
|
|
2577
|
-
catch (error) {
|
|
2578
|
-
getLogger().error('Error submitting bug report:', error);
|
|
2579
|
-
// Show error message in modal instead of blocking alert
|
|
2580
|
-
elements.submitError.textContent =
|
|
2581
|
-
'Failed to submit bug report. Please try again.';
|
|
2582
|
-
elements.submitError.style.display = 'block';
|
|
2583
|
-
// Clear stale progress status for screen readers
|
|
2584
|
-
elements.progressStatus.textContent = '';
|
|
2585
|
-
// Re-enable submit button on error
|
|
2586
|
-
elements.submitButton.disabled = false;
|
|
2587
|
-
elements.submitButton.textContent = originalButtonText;
|
|
2588
|
-
elements.submitButton.classList.remove('loading');
|
|
2589
|
-
}
|
|
2908
|
+
});
|
|
2590
2909
|
}
|
|
2591
2910
|
/**
|
|
2592
2911
|
* Get the final screenshot (with redactions applied)
|
|
@@ -2618,64 +2937,6 @@ const DEFAULT_REPLAY_DURATION_SECONDS = 15;
|
|
|
2618
2937
|
*/
|
|
2619
2938
|
const MAX_RECOMMENDED_REPLAY_DURATION_SECONDS = 30;
|
|
2620
2939
|
|
|
2621
|
-
/**
|
|
2622
|
-
* URL Helper Utilities
|
|
2623
|
-
* Extract base API URL from endpoint configuration
|
|
2624
|
-
*/
|
|
2625
|
-
const logger$7 = getLogger();
|
|
2626
|
-
/**
|
|
2627
|
-
* Custom error for invalid endpoint URLs
|
|
2628
|
-
*/
|
|
2629
|
-
class InvalidEndpointError extends Error {
|
|
2630
|
-
constructor(endpoint, reason) {
|
|
2631
|
-
super(`Invalid endpoint URL: ${endpoint}. ${reason}`);
|
|
2632
|
-
this.endpoint = endpoint;
|
|
2633
|
-
this.reason = reason;
|
|
2634
|
-
this.name = 'InvalidEndpointError';
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
/**
|
|
2638
|
-
* Strip known endpoint suffixes from path
|
|
2639
|
-
* Removes /api/v1/reports path
|
|
2640
|
-
*/
|
|
2641
|
-
function stripEndpointSuffix(path) {
|
|
2642
|
-
// Use lastIndexOf to handle paths like '/prefix/api/v1/reports'
|
|
2643
|
-
const reportsIndex = path.lastIndexOf('/api/v1/reports');
|
|
2644
|
-
if (reportsIndex !== -1) {
|
|
2645
|
-
return path.substring(0, reportsIndex);
|
|
2646
|
-
}
|
|
2647
|
-
// Remove trailing slash
|
|
2648
|
-
return path.replace(/\/$/, '') || '';
|
|
2649
|
-
}
|
|
2650
|
-
/**
|
|
2651
|
-
* Extract base API URL from endpoint
|
|
2652
|
-
* Returns scheme + host + base path (without /api/v1/reports suffix)
|
|
2653
|
-
*
|
|
2654
|
-
* @example
|
|
2655
|
-
* getApiBaseUrl('https://api.example.com/api/v1/reports')
|
|
2656
|
-
* // Returns: 'https://api.example.com'
|
|
2657
|
-
*
|
|
2658
|
-
* @throws InvalidEndpointError if endpoint is not a valid absolute URL
|
|
2659
|
-
*/
|
|
2660
|
-
function getApiBaseUrl(endpoint) {
|
|
2661
|
-
if (!endpoint) {
|
|
2662
|
-
throw new InvalidEndpointError('', 'No endpoint configured');
|
|
2663
|
-
}
|
|
2664
|
-
try {
|
|
2665
|
-
const url = new URL(endpoint);
|
|
2666
|
-
const basePath = stripEndpointSuffix(url.pathname);
|
|
2667
|
-
return url.origin + basePath;
|
|
2668
|
-
}
|
|
2669
|
-
catch (error) {
|
|
2670
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2671
|
-
logger$7.error('Invalid endpoint URL - must be a valid absolute URL', {
|
|
2672
|
-
endpoint,
|
|
2673
|
-
error: errorMessage,
|
|
2674
|
-
});
|
|
2675
|
-
throw new InvalidEndpointError(endpoint, 'Must be a valid absolute URL (e.g., https://api.example.com/api/v1/reports)');
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
2940
|
/**
|
|
2680
2941
|
* SDK version - auto-generated from package.json
|
|
2681
2942
|
* DO NOT EDIT THIS FILE MANUALLY
|
|
@@ -2683,7 +2944,7 @@ function getApiBaseUrl(endpoint) {
|
|
|
2683
2944
|
* This file is automatically generated during the build process.
|
|
2684
2945
|
* To update the version, modify package.json
|
|
2685
2946
|
*/
|
|
2686
|
-
const VERSION = '
|
|
2947
|
+
const VERSION = '1.1.0';
|
|
2687
2948
|
|
|
2688
2949
|
/**
|
|
2689
2950
|
* Configuration Validation Utilities
|
|
@@ -2697,6 +2958,11 @@ function validateAuthConfig(context) {
|
|
|
2697
2958
|
if (!context.endpoint) {
|
|
2698
2959
|
throw new Error('No endpoint configured for bug report submission');
|
|
2699
2960
|
}
|
|
2961
|
+
// SECURITY: Ensure endpoint uses HTTPS
|
|
2962
|
+
// This prevents credentials and sensitive data from being sent over plain HTTP
|
|
2963
|
+
if (!isSecureEndpoint(context.endpoint)) {
|
|
2964
|
+
throw new InsecureEndpointError(context.endpoint);
|
|
2965
|
+
}
|
|
2700
2966
|
if (!context.auth) {
|
|
2701
2967
|
throw new Error('API key authentication is required');
|
|
2702
2968
|
}
|
|
@@ -10564,21 +10830,23 @@ function dataToString(data) {
|
|
|
10564
10830
|
* @param config - Optional compression configuration
|
|
10565
10831
|
* @returns Compressed data as Uint8Array
|
|
10566
10832
|
*/
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10833
|
+
function compressData(data, config) {
|
|
10834
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
10835
|
+
var _a;
|
|
10836
|
+
try {
|
|
10837
|
+
const jsonString = dataToString(data);
|
|
10838
|
+
const encoder = getTextEncoder();
|
|
10839
|
+
const uint8Data = encoder.encode(jsonString);
|
|
10840
|
+
const gzipLevel = (_a = config === null || config === void 0 ? void 0 : config.gzipLevel) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.GZIP_LEVEL;
|
|
10841
|
+
// pako.gzip already returns Uint8Array, no need to wrap it
|
|
10842
|
+
const compressed = pako.gzip(uint8Data, { level: gzipLevel });
|
|
10843
|
+
return compressed;
|
|
10844
|
+
}
|
|
10845
|
+
catch (error) {
|
|
10846
|
+
logger$5.error('Compression failed:', error);
|
|
10847
|
+
throw error;
|
|
10848
|
+
}
|
|
10849
|
+
});
|
|
10582
10850
|
}
|
|
10583
10851
|
/**
|
|
10584
10852
|
* Try to parse string as JSON, return string if not valid JSON
|
|
@@ -10684,43 +10952,45 @@ function calculateResizedDimensions(width, height, maxWidth, maxHeight) {
|
|
|
10684
10952
|
* @param config - Optional compression configuration
|
|
10685
10953
|
* @returns Optimized base64 image string
|
|
10686
10954
|
*/
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10955
|
+
function compressImage(base64, config) {
|
|
10956
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
10957
|
+
var _a, _b, _c, _d;
|
|
10958
|
+
try {
|
|
10959
|
+
if (!isBrowserEnvironment()) {
|
|
10960
|
+
return base64;
|
|
10961
|
+
}
|
|
10962
|
+
const maxWidth = (_a = config === null || config === void 0 ? void 0 : config.imageMaxWidth) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.IMAGE_MAX_WIDTH;
|
|
10963
|
+
const maxHeight = (_b = config === null || config === void 0 ? void 0 : config.imageMaxHeight) !== null && _b !== void 0 ? _b : COMPRESSION_DEFAULTS.IMAGE_MAX_HEIGHT;
|
|
10964
|
+
const webpQuality = (_c = config === null || config === void 0 ? void 0 : config.webpQuality) !== null && _c !== void 0 ? _c : COMPRESSION_DEFAULTS.IMAGE_WEBP_QUALITY;
|
|
10965
|
+
const jpegQuality = (_d = config === null || config === void 0 ? void 0 : config.jpegQuality) !== null && _d !== void 0 ? _d : COMPRESSION_DEFAULTS.IMAGE_JPEG_QUALITY;
|
|
10966
|
+
const timeout = COMPRESSION_DEFAULTS.IMAGE_LOAD_TIMEOUT;
|
|
10967
|
+
const img = yield loadImage(base64, timeout);
|
|
10968
|
+
const canvas = document.createElement('canvas');
|
|
10969
|
+
const ctx = canvas.getContext('2d');
|
|
10970
|
+
if (!ctx) {
|
|
10971
|
+
throw new Error('Failed to get 2D canvas context');
|
|
10972
|
+
}
|
|
10973
|
+
const { width, height } = calculateResizedDimensions(img.width, img.height, maxWidth, maxHeight);
|
|
10974
|
+
canvas.width = width;
|
|
10975
|
+
canvas.height = height;
|
|
10976
|
+
// Enable high-quality image smoothing for better resize quality
|
|
10977
|
+
ctx.imageSmoothingEnabled = true;
|
|
10978
|
+
ctx.imageSmoothingQuality = 'high';
|
|
10979
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
10980
|
+
if (supportsWebP()) {
|
|
10981
|
+
return canvas.toDataURL('image/webp', webpQuality);
|
|
10982
|
+
}
|
|
10983
|
+
else {
|
|
10984
|
+
return canvas.toDataURL('image/jpeg', jpegQuality);
|
|
10985
|
+
}
|
|
10716
10986
|
}
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10987
|
+
catch (error) {
|
|
10988
|
+
if ((config === null || config === void 0 ? void 0 : config.verbose) !== false) {
|
|
10989
|
+
getLogger().error('Image compression failed:', error);
|
|
10990
|
+
}
|
|
10991
|
+
return base64;
|
|
10721
10992
|
}
|
|
10722
|
-
|
|
10723
|
-
}
|
|
10993
|
+
});
|
|
10724
10994
|
}
|
|
10725
10995
|
/**
|
|
10726
10996
|
* Calculate compression ratio for analytics
|
|
@@ -10774,100 +11044,25 @@ class ScreenshotCapture extends BaseCapture {
|
|
|
10774
11044
|
return this.shouldIncludeNode(node);
|
|
10775
11045
|
} });
|
|
10776
11046
|
}
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
11047
|
+
capture(targetElement) {
|
|
11048
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
11049
|
+
try {
|
|
11050
|
+
const element = targetElement || this.options.targetElement || document.body;
|
|
11051
|
+
const options = this.buildCaptureOptions();
|
|
11052
|
+
const dataUrl = yield toPng(element, options);
|
|
11053
|
+
// Compress the screenshot to reduce payload size
|
|
11054
|
+
// Converts to WebP if supported, resizes if too large
|
|
11055
|
+
const compressed = yield compressImage(dataUrl);
|
|
11056
|
+
return compressed;
|
|
11057
|
+
}
|
|
11058
|
+
catch (error) {
|
|
11059
|
+
this.handleError('capturing screenshot', error);
|
|
11060
|
+
return this.getErrorPlaceholder();
|
|
11061
|
+
}
|
|
11062
|
+
});
|
|
10791
11063
|
}
|
|
10792
11064
|
}
|
|
10793
11065
|
|
|
10794
|
-
/**
|
|
10795
|
-
* A generic circular buffer implementation for storing a fixed number of items.
|
|
10796
|
-
* When the buffer is full, new items overwrite the oldest items.
|
|
10797
|
-
*
|
|
10798
|
-
* @template T The type of items stored in the buffer
|
|
10799
|
-
*/
|
|
10800
|
-
let CircularBuffer$1 = class CircularBuffer {
|
|
10801
|
-
constructor(maxSize) {
|
|
10802
|
-
this.maxSize = maxSize;
|
|
10803
|
-
this.items = [];
|
|
10804
|
-
this.index = 0;
|
|
10805
|
-
this.count = 0;
|
|
10806
|
-
if (maxSize <= 0) {
|
|
10807
|
-
throw new Error('CircularBuffer maxSize must be greater than 0');
|
|
10808
|
-
}
|
|
10809
|
-
}
|
|
10810
|
-
/**
|
|
10811
|
-
* Add an item to the buffer. If the buffer is full, the oldest item is overwritten.
|
|
10812
|
-
*/
|
|
10813
|
-
add(item) {
|
|
10814
|
-
if (this.count < this.maxSize) {
|
|
10815
|
-
this.items.push(item);
|
|
10816
|
-
this.count++;
|
|
10817
|
-
}
|
|
10818
|
-
else {
|
|
10819
|
-
this.items[this.index] = item;
|
|
10820
|
-
}
|
|
10821
|
-
this.index = (this.index + 1) % this.maxSize;
|
|
10822
|
-
}
|
|
10823
|
-
/**
|
|
10824
|
-
* Get all items in chronological order (oldest to newest).
|
|
10825
|
-
* Returns a copy of the internal array.
|
|
10826
|
-
*/
|
|
10827
|
-
getAll() {
|
|
10828
|
-
if (this.count < this.maxSize) {
|
|
10829
|
-
return [...this.items];
|
|
10830
|
-
}
|
|
10831
|
-
// Return items in chronological order when buffer is full
|
|
10832
|
-
return [
|
|
10833
|
-
...this.items.slice(this.index),
|
|
10834
|
-
...this.items.slice(0, this.index),
|
|
10835
|
-
];
|
|
10836
|
-
}
|
|
10837
|
-
/**
|
|
10838
|
-
* Clear all items from the buffer.
|
|
10839
|
-
*/
|
|
10840
|
-
clear() {
|
|
10841
|
-
this.items = [];
|
|
10842
|
-
this.index = 0;
|
|
10843
|
-
this.count = 0;
|
|
10844
|
-
}
|
|
10845
|
-
/**
|
|
10846
|
-
* Get the current number of items in the buffer.
|
|
10847
|
-
*/
|
|
10848
|
-
get size() {
|
|
10849
|
-
return this.count;
|
|
10850
|
-
}
|
|
10851
|
-
/**
|
|
10852
|
-
* Get the maximum capacity of the buffer.
|
|
10853
|
-
*/
|
|
10854
|
-
get capacity() {
|
|
10855
|
-
return this.maxSize;
|
|
10856
|
-
}
|
|
10857
|
-
/**
|
|
10858
|
-
* Check if the buffer is empty.
|
|
10859
|
-
*/
|
|
10860
|
-
get isEmpty() {
|
|
10861
|
-
return this.count === 0;
|
|
10862
|
-
}
|
|
10863
|
-
/**
|
|
10864
|
-
* Check if the buffer is full.
|
|
10865
|
-
*/
|
|
10866
|
-
get isFull() {
|
|
10867
|
-
return this.count >= this.maxSize;
|
|
10868
|
-
}
|
|
10869
|
-
};
|
|
10870
|
-
|
|
10871
11066
|
const CONSOLE_METHODS = [
|
|
10872
11067
|
'log',
|
|
10873
11068
|
'warn',
|
|
@@ -11072,7 +11267,7 @@ class NetworkCapture extends BaseCapture {
|
|
|
11072
11267
|
}
|
|
11073
11268
|
interceptFetch() {
|
|
11074
11269
|
const originalFetch = this.originalFetch;
|
|
11075
|
-
window.fetch =
|
|
11270
|
+
window.fetch = (...args) => __awaiter$1(this, void 0, void 0, function* () {
|
|
11076
11271
|
const startTime = Date.now();
|
|
11077
11272
|
let url = '';
|
|
11078
11273
|
let method = 'GET';
|
|
@@ -11083,7 +11278,7 @@ class NetworkCapture extends BaseCapture {
|
|
|
11083
11278
|
this.handleError('parsing fetch arguments', error);
|
|
11084
11279
|
}
|
|
11085
11280
|
try {
|
|
11086
|
-
const response =
|
|
11281
|
+
const response = yield originalFetch(...args);
|
|
11087
11282
|
// Only log if response is valid (handles mocked data URLs that return undefined)
|
|
11088
11283
|
if (response && typeof response.status === 'number') {
|
|
11089
11284
|
const request = this.createNetworkRequest(url, method, response.status, startTime);
|
|
@@ -11096,7 +11291,7 @@ class NetworkCapture extends BaseCapture {
|
|
|
11096
11291
|
this.addRequest(request);
|
|
11097
11292
|
throw error;
|
|
11098
11293
|
}
|
|
11099
|
-
};
|
|
11294
|
+
});
|
|
11100
11295
|
}
|
|
11101
11296
|
interceptXHR() {
|
|
11102
11297
|
const originalOpen = this.originalXHR.open;
|
|
@@ -14122,7 +14317,7 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
|
14122
14317
|
PERFORMANCE OF THIS SOFTWARE.
|
|
14123
14318
|
***************************************************************************** */
|
|
14124
14319
|
|
|
14125
|
-
function __rest
|
|
14320
|
+
function __rest(s, e) {
|
|
14126
14321
|
var t = {};
|
|
14127
14322
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14128
14323
|
t[p] = s[p];
|
|
@@ -14644,7 +14839,7 @@ class CanvasManager {
|
|
|
14644
14839
|
if (!valuesWithType || id === -1)
|
|
14645
14840
|
return;
|
|
14646
14841
|
const values = valuesWithType.map((value) => {
|
|
14647
|
-
const rest = __rest
|
|
14842
|
+
const rest = __rest(value, ["type"]);
|
|
14648
14843
|
return rest;
|
|
14649
14844
|
});
|
|
14650
14845
|
const { type } = valuesWithType[0];
|
|
@@ -15439,28 +15634,32 @@ class CaptureManager {
|
|
|
15439
15634
|
/**
|
|
15440
15635
|
* Capture all data for bug report
|
|
15441
15636
|
*/
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
|
|
15455
|
-
|
|
15456
|
-
|
|
15457
|
-
|
|
15637
|
+
captureAll() {
|
|
15638
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15639
|
+
var _a, _b;
|
|
15640
|
+
// Call synchronous methods directly
|
|
15641
|
+
const consoleLogs = this.console.getLogs();
|
|
15642
|
+
const networkRequests = this.network.getRequests();
|
|
15643
|
+
const metadata = this.metadata.capture();
|
|
15644
|
+
const replay = (_b = (_a = this.domCollector) === null || _a === void 0 ? void 0 : _a.getEvents()) !== null && _b !== void 0 ? _b : [];
|
|
15645
|
+
// Await async screenshot capture
|
|
15646
|
+
const screenshotPreview = yield this.screenshot.capture();
|
|
15647
|
+
return {
|
|
15648
|
+
console: consoleLogs,
|
|
15649
|
+
network: networkRequests,
|
|
15650
|
+
metadata,
|
|
15651
|
+
replay,
|
|
15652
|
+
_screenshotPreview: screenshotPreview,
|
|
15653
|
+
};
|
|
15654
|
+
});
|
|
15458
15655
|
}
|
|
15459
15656
|
/**
|
|
15460
15657
|
* Get screenshot only
|
|
15461
15658
|
*/
|
|
15462
|
-
|
|
15463
|
-
return
|
|
15659
|
+
captureScreenshot() {
|
|
15660
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15661
|
+
return yield this.screenshot.capture();
|
|
15662
|
+
});
|
|
15464
15663
|
}
|
|
15465
15664
|
/**
|
|
15466
15665
|
* Cleanup resources
|
|
@@ -15473,40 +15672,6 @@ class CaptureManager {
|
|
|
15473
15672
|
}
|
|
15474
15673
|
}
|
|
15475
15674
|
|
|
15476
|
-
/******************************************************************************
|
|
15477
|
-
Copyright (c) Microsoft Corporation.
|
|
15478
|
-
|
|
15479
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
15480
|
-
purpose with or without fee is hereby granted.
|
|
15481
|
-
|
|
15482
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
15483
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
15484
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
15485
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
15486
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
15487
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15488
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
15489
|
-
***************************************************************************** */
|
|
15490
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
function __rest(s, e) {
|
|
15494
|
-
var t = {};
|
|
15495
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
15496
|
-
t[p] = s[p];
|
|
15497
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15498
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
15499
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
15500
|
-
t[p[i]] = s[p[i]];
|
|
15501
|
-
}
|
|
15502
|
-
return t;
|
|
15503
|
-
}
|
|
15504
|
-
|
|
15505
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
15506
|
-
var e = new Error(message);
|
|
15507
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
15508
|
-
};
|
|
15509
|
-
|
|
15510
15675
|
const logger$3 = getLogger();
|
|
15511
15676
|
/**
|
|
15512
15677
|
* Handles file upload operations using presigned URLs
|
|
@@ -15525,111 +15690,119 @@ class FileUploadHandler {
|
|
|
15525
15690
|
* Orchestrates the complete file upload flow
|
|
15526
15691
|
* @throws Error if any step fails
|
|
15527
15692
|
*/
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15693
|
+
uploadFiles(bugId, report, presignedUrls) {
|
|
15694
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15695
|
+
const filesToUpload = yield this.prepareFiles(report, presignedUrls);
|
|
15696
|
+
if (filesToUpload.length === 0) {
|
|
15697
|
+
return; // No files to upload
|
|
15698
|
+
}
|
|
15699
|
+
yield this.uploadToStorage(filesToUpload);
|
|
15700
|
+
yield this.confirmUploads(filesToUpload, bugId);
|
|
15701
|
+
});
|
|
15535
15702
|
}
|
|
15536
15703
|
/**
|
|
15537
15704
|
* Prepare file blobs and validate presigned URLs
|
|
15538
15705
|
*/
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
report._screenshotPreview
|
|
15544
|
-
|
|
15545
|
-
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15549
|
-
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
|
|
15562
|
-
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15566
|
-
|
|
15567
|
-
|
|
15706
|
+
prepareFiles(report, presignedUrls) {
|
|
15707
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15708
|
+
const files = [];
|
|
15709
|
+
// Prepare screenshot
|
|
15710
|
+
if (report._screenshotPreview &&
|
|
15711
|
+
report._screenshotPreview.startsWith('data:image/')) {
|
|
15712
|
+
const screenshotUrl = this.getPresignedUrl('screenshot', presignedUrls);
|
|
15713
|
+
const screenshotBlob = yield this.dataUrlToBlob(report._screenshotPreview);
|
|
15714
|
+
files.push({
|
|
15715
|
+
type: 'screenshot',
|
|
15716
|
+
url: screenshotUrl.uploadUrl,
|
|
15717
|
+
key: screenshotUrl.storageKey,
|
|
15718
|
+
blob: screenshotBlob,
|
|
15719
|
+
});
|
|
15720
|
+
}
|
|
15721
|
+
// Prepare replay
|
|
15722
|
+
if (report.replay && report.replay.length > 0) {
|
|
15723
|
+
const replayUrl = this.getPresignedUrl('replay', presignedUrls);
|
|
15724
|
+
const compressed = yield compressData(report.replay);
|
|
15725
|
+
const replayBlob = new Blob([compressed], {
|
|
15726
|
+
type: 'application/gzip',
|
|
15727
|
+
});
|
|
15728
|
+
files.push({
|
|
15729
|
+
type: 'replay',
|
|
15730
|
+
url: replayUrl.uploadUrl,
|
|
15731
|
+
key: replayUrl.storageKey,
|
|
15732
|
+
blob: replayBlob,
|
|
15733
|
+
});
|
|
15734
|
+
}
|
|
15735
|
+
return files;
|
|
15736
|
+
});
|
|
15568
15737
|
}
|
|
15569
15738
|
/**
|
|
15570
15739
|
* Upload files to storage using presigned URLs (parallel execution)
|
|
15571
15740
|
* Note: No custom headers should be added - presigned URLs are pre-signed with specific headers.
|
|
15572
15741
|
* Adding headers like Content-Type that weren't included in the signature will cause 403 errors.
|
|
15573
15742
|
*/
|
|
15574
|
-
|
|
15575
|
-
|
|
15576
|
-
const
|
|
15577
|
-
|
|
15578
|
-
|
|
15579
|
-
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
|
|
15743
|
+
uploadToStorage(files) {
|
|
15744
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15745
|
+
const uploadPromises = files.map((file) => __awaiter$1(this, void 0, void 0, function* () {
|
|
15746
|
+
const controller = new AbortController();
|
|
15747
|
+
const timeoutId = setTimeout(() => controller.abort(), FileUploadHandler.UPLOAD_TIMEOUT_MS);
|
|
15748
|
+
try {
|
|
15749
|
+
// Do NOT add Content-Type header - it's already included in the presigned URL signature
|
|
15750
|
+
// Adding it here will cause a signature mismatch and 403 Forbidden error
|
|
15751
|
+
const response = yield fetch(file.url, {
|
|
15752
|
+
method: 'PUT',
|
|
15753
|
+
body: file.blob,
|
|
15754
|
+
signal: controller.signal,
|
|
15755
|
+
});
|
|
15756
|
+
clearTimeout(timeoutId);
|
|
15757
|
+
return { success: response.ok, type: file.type };
|
|
15758
|
+
}
|
|
15759
|
+
catch (error) {
|
|
15760
|
+
clearTimeout(timeoutId);
|
|
15761
|
+
logger$3.error(`Upload failed for ${file.type}:`, error);
|
|
15762
|
+
return { success: false, type: file.type };
|
|
15763
|
+
}
|
|
15764
|
+
}));
|
|
15765
|
+
const results = yield Promise.all(uploadPromises);
|
|
15766
|
+
// Check for upload failures
|
|
15767
|
+
for (const result of results) {
|
|
15768
|
+
if (!result.success) {
|
|
15769
|
+
throw new Error(`${this.formatFileType(result.type)} upload failed: Upload to storage failed`);
|
|
15770
|
+
}
|
|
15593
15771
|
}
|
|
15594
15772
|
});
|
|
15595
|
-
const results = await Promise.all(uploadPromises);
|
|
15596
|
-
// Check for upload failures
|
|
15597
|
-
for (const result of results) {
|
|
15598
|
-
if (!result.success) {
|
|
15599
|
-
throw new Error(`${this.formatFileType(result.type)} upload failed: Upload to storage failed`);
|
|
15600
|
-
}
|
|
15601
|
-
}
|
|
15602
15773
|
}
|
|
15603
15774
|
/**
|
|
15604
15775
|
* Confirm uploads with backend (parallel execution)
|
|
15605
15776
|
*/
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15777
|
+
confirmUploads(files, bugId) {
|
|
15778
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15779
|
+
const confirmPromises = files.map((file) => __awaiter$1(this, void 0, void 0, function* () {
|
|
15780
|
+
try {
|
|
15781
|
+
const response = yield fetch(`${this.apiEndpoint}/api/v1/reports/${bugId}/confirm-upload`, {
|
|
15782
|
+
method: 'POST',
|
|
15783
|
+
headers: {
|
|
15784
|
+
'Content-Type': 'application/json',
|
|
15785
|
+
'X-API-Key': this.apiKey,
|
|
15786
|
+
},
|
|
15787
|
+
body: JSON.stringify({
|
|
15788
|
+
fileType: file.type,
|
|
15789
|
+
}),
|
|
15790
|
+
});
|
|
15791
|
+
return { success: response.ok, type: file.type };
|
|
15792
|
+
}
|
|
15793
|
+
catch (error) {
|
|
15794
|
+
logger$3.error(`Confirmation failed for ${file.type}:`, error);
|
|
15795
|
+
return { success: false, type: file.type };
|
|
15796
|
+
}
|
|
15797
|
+
}));
|
|
15798
|
+
const results = yield Promise.all(confirmPromises);
|
|
15799
|
+
// Check for confirmation failures
|
|
15800
|
+
for (const result of results) {
|
|
15801
|
+
if (!result.success) {
|
|
15802
|
+
throw new Error(`${this.formatFileType(result.type)} confirmation failed: Backend did not acknowledge upload`);
|
|
15803
|
+
}
|
|
15624
15804
|
}
|
|
15625
15805
|
});
|
|
15626
|
-
const results = await Promise.all(confirmPromises);
|
|
15627
|
-
// Check for confirmation failures
|
|
15628
|
-
for (const result of results) {
|
|
15629
|
-
if (!result.success) {
|
|
15630
|
-
throw new Error(`${this.formatFileType(result.type)} confirmation failed: Backend did not acknowledge upload`);
|
|
15631
|
-
}
|
|
15632
|
-
}
|
|
15633
15806
|
}
|
|
15634
15807
|
/**
|
|
15635
15808
|
* Get presigned URL with validation
|
|
@@ -15644,15 +15817,17 @@ class FileUploadHandler {
|
|
|
15644
15817
|
/**
|
|
15645
15818
|
* Convert data URL to Blob
|
|
15646
15819
|
*/
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15820
|
+
dataUrlToBlob(dataUrl) {
|
|
15821
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
15822
|
+
if (!dataUrl || !dataUrl.startsWith('data:')) {
|
|
15823
|
+
throw new Error('Invalid data URL');
|
|
15824
|
+
}
|
|
15825
|
+
const response = yield fetch(dataUrl);
|
|
15826
|
+
if (!response || !response.blob) {
|
|
15827
|
+
throw new Error('Failed to convert data URL to Blob');
|
|
15828
|
+
}
|
|
15829
|
+
return yield response.blob();
|
|
15830
|
+
});
|
|
15656
15831
|
}
|
|
15657
15832
|
/**
|
|
15658
15833
|
* Format file type for error messages (capitalize first letter)
|
|
@@ -15904,6 +16079,18 @@ const DEFAULT_OFFLINE_CONFIG$1 = {
|
|
|
15904
16079
|
enabled: false,
|
|
15905
16080
|
maxQueueSize: 10,
|
|
15906
16081
|
};
|
|
16082
|
+
/**
|
|
16083
|
+
* Set of sensitive header names that should be stripped before localStorage storage
|
|
16084
|
+
* SECURITY: Using Set for O(1) lookup performance
|
|
16085
|
+
*/
|
|
16086
|
+
const SENSITIVE_HEADERS = new Set([
|
|
16087
|
+
'authorization',
|
|
16088
|
+
'x-api-key',
|
|
16089
|
+
'x-auth-token',
|
|
16090
|
+
'x-access-token',
|
|
16091
|
+
'cookie',
|
|
16092
|
+
'set-cookie',
|
|
16093
|
+
]);
|
|
15907
16094
|
// ============================================================================
|
|
15908
16095
|
// OFFLINE QUEUE CLASS
|
|
15909
16096
|
// ============================================================================
|
|
@@ -15916,6 +16103,7 @@ class OfflineQueue {
|
|
|
15916
16103
|
}
|
|
15917
16104
|
/**
|
|
15918
16105
|
* Queue a request for offline retry
|
|
16106
|
+
* SECURITY: Strips sensitive authentication headers before storage
|
|
15919
16107
|
*/
|
|
15920
16108
|
enqueue(endpoint, body, headers) {
|
|
15921
16109
|
try {
|
|
@@ -15929,7 +16117,9 @@ class OfflineQueue {
|
|
|
15929
16117
|
this.logger.warn(`Offline queue is full (${this.config.maxQueueSize}), removing oldest request`);
|
|
15930
16118
|
queue.shift();
|
|
15931
16119
|
}
|
|
15932
|
-
|
|
16120
|
+
// SECURITY: Strip sensitive headers before storing in localStorage
|
|
16121
|
+
const sanitizedHeaders = this.stripSensitiveHeaders(headers);
|
|
16122
|
+
queue.push(this.createQueuedRequest(endpoint, serializedBody, sanitizedHeaders));
|
|
15933
16123
|
this.saveQueue(queue);
|
|
15934
16124
|
this.logger.log(`Request queued for offline retry (queue size: ${queue.length})`);
|
|
15935
16125
|
}
|
|
@@ -15939,62 +16129,88 @@ class OfflineQueue {
|
|
|
15939
16129
|
}
|
|
15940
16130
|
/**
|
|
15941
16131
|
* Process offline queue
|
|
16132
|
+
* @deprecated Use processWithAuth() instead to properly handle authentication
|
|
15942
16133
|
*/
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
const age = Date.now() - request.timestamp;
|
|
15959
|
-
const maxAge = QUEUE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
|
|
15960
|
-
if (age > maxAge) {
|
|
15961
|
-
this.logger.warn(`Removing expired queued request (id: ${request.id})`);
|
|
15962
|
-
continue;
|
|
16134
|
+
process(retryableStatusCodes) {
|
|
16135
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16136
|
+
return this.processWithAuth(retryableStatusCodes, {});
|
|
16137
|
+
});
|
|
16138
|
+
}
|
|
16139
|
+
/**
|
|
16140
|
+
* Process offline queue with authentication headers
|
|
16141
|
+
* @param retryableStatusCodes - HTTP status codes that should be retried
|
|
16142
|
+
* @param authHeaders - Authentication headers to merge with stored headers
|
|
16143
|
+
*/
|
|
16144
|
+
processWithAuth(retryableStatusCodes, authHeaders) {
|
|
16145
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16146
|
+
const queue = this.getQueue();
|
|
16147
|
+
if (queue.length === 0) {
|
|
16148
|
+
return;
|
|
15963
16149
|
}
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
15970
|
-
|
|
15971
|
-
|
|
15972
|
-
|
|
15973
|
-
|
|
16150
|
+
this.logger.log(`Processing offline queue (${queue.length} requests)`);
|
|
16151
|
+
const successfulIds = [];
|
|
16152
|
+
const rejectedIds = [];
|
|
16153
|
+
const failedRequests = [];
|
|
16154
|
+
for (const request of queue) {
|
|
16155
|
+
// Check if request has exceeded max retry attempts
|
|
16156
|
+
if (request.attempts >= MAX_RETRY_ATTEMPTS) {
|
|
16157
|
+
this.logger.warn(`Max retry attempts (${MAX_RETRY_ATTEMPTS}) reached for request (id: ${request.id}), removing`);
|
|
16158
|
+
continue;
|
|
16159
|
+
}
|
|
16160
|
+
// Check if request has expired
|
|
16161
|
+
const age = Date.now() - request.timestamp;
|
|
16162
|
+
const maxAge = QUEUE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
|
|
16163
|
+
if (age > maxAge) {
|
|
16164
|
+
this.logger.warn(`Removing expired queued request (id: ${request.id})`);
|
|
16165
|
+
continue;
|
|
15974
16166
|
}
|
|
15975
|
-
|
|
15976
|
-
//
|
|
16167
|
+
try {
|
|
16168
|
+
// SECURITY: Verify endpoint is secure (HTTPS) before sending
|
|
16169
|
+
// This prevents downgrade attacks and ensures data confidentiality
|
|
16170
|
+
if (!isSecureEndpoint(request.endpoint)) {
|
|
16171
|
+
this.logger.error(`Refusing to send offline request to insecure endpoint: ${request.endpoint}`);
|
|
16172
|
+
rejectedIds.push(request.id);
|
|
16173
|
+
// Don't retry insecure requests
|
|
16174
|
+
continue;
|
|
16175
|
+
}
|
|
16176
|
+
// Merge auth headers with stored headers (auth headers take precedence)
|
|
16177
|
+
const headers = Object.assign(Object.assign({}, request.headers), authHeaders);
|
|
16178
|
+
// Attempt to send
|
|
16179
|
+
const response = yield fetch(request.endpoint, {
|
|
16180
|
+
method: 'POST',
|
|
16181
|
+
headers,
|
|
16182
|
+
body: request.body,
|
|
16183
|
+
});
|
|
16184
|
+
if (response.ok) {
|
|
16185
|
+
this.logger.log(`Successfully sent queued request (id: ${request.id})`);
|
|
16186
|
+
successfulIds.push(request.id);
|
|
16187
|
+
}
|
|
16188
|
+
else if (retryableStatusCodes.includes(response.status)) {
|
|
16189
|
+
// Keep in queue for next attempt
|
|
16190
|
+
request.attempts++;
|
|
16191
|
+
failedRequests.push(request);
|
|
16192
|
+
this.logger.warn(`Queued request failed with status ${response.status}, will retry later (id: ${request.id})`);
|
|
16193
|
+
}
|
|
16194
|
+
else {
|
|
16195
|
+
// Non-retryable error, remove from queue
|
|
16196
|
+
this.logger.warn(`Queued request failed with non-retryable status ${response.status}, removing (id: ${request.id})`);
|
|
16197
|
+
}
|
|
16198
|
+
}
|
|
16199
|
+
catch (error) {
|
|
16200
|
+
// Network error, keep in queue
|
|
15977
16201
|
request.attempts++;
|
|
15978
16202
|
failedRequests.push(request);
|
|
15979
|
-
this.logger.warn(`Queued request failed with
|
|
15980
|
-
}
|
|
15981
|
-
else {
|
|
15982
|
-
// Non-retryable error, remove from queue
|
|
15983
|
-
this.logger.warn(`Queued request failed with non-retryable status ${response.status}, removing (id: ${request.id})`);
|
|
16203
|
+
this.logger.warn(`Queued request failed with network error, will retry later (id: ${request.id}):`, error);
|
|
15984
16204
|
}
|
|
15985
16205
|
}
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
16206
|
+
// Update queue (remove successful and expired, keep failed)
|
|
16207
|
+
this.saveQueue(failedRequests);
|
|
16208
|
+
if (successfulIds.length > 0 ||
|
|
16209
|
+
rejectedIds.length > 0 ||
|
|
16210
|
+
failedRequests.length < queue.length) {
|
|
16211
|
+
this.logger.log(`Offline queue processed: ${successfulIds.length} successful, ${rejectedIds.length} rejected (insecure), ${failedRequests.length} remaining`);
|
|
15991
16212
|
}
|
|
15992
|
-
}
|
|
15993
|
-
// Update queue (remove successful and expired, keep failed)
|
|
15994
|
-
this.saveQueue(failedRequests);
|
|
15995
|
-
if (successfulIds.length > 0 || failedRequests.length < queue.length) {
|
|
15996
|
-
this.logger.log(`Offline queue processed: ${successfulIds.length} successful, ${failedRequests.length} remaining`);
|
|
15997
|
-
}
|
|
16213
|
+
});
|
|
15998
16214
|
}
|
|
15999
16215
|
/**
|
|
16000
16216
|
* Clear offline queue
|
|
@@ -16016,6 +16232,13 @@ class OfflineQueue {
|
|
|
16016
16232
|
// ============================================================================
|
|
16017
16233
|
// PRIVATE METHODS
|
|
16018
16234
|
// ============================================================================
|
|
16235
|
+
/**
|
|
16236
|
+
* Strip sensitive authentication headers before storing in localStorage
|
|
16237
|
+
* SECURITY: Prevents API keys and tokens from being stored in plain text
|
|
16238
|
+
*/
|
|
16239
|
+
stripSensitiveHeaders(headers) {
|
|
16240
|
+
return Object.fromEntries(Object.entries(headers).filter(([key]) => !SENSITIVE_HEADERS.has(key.toLowerCase())));
|
|
16241
|
+
}
|
|
16019
16242
|
/**
|
|
16020
16243
|
* Serialize body to string format
|
|
16021
16244
|
*/
|
|
@@ -16206,39 +16429,41 @@ class RetryHandler {
|
|
|
16206
16429
|
/**
|
|
16207
16430
|
* Execute operation with exponential backoff retry
|
|
16208
16431
|
*/
|
|
16209
|
-
|
|
16210
|
-
|
|
16211
|
-
|
|
16212
|
-
|
|
16213
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
16216
|
-
|
|
16217
|
-
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
catch (error) {
|
|
16226
|
-
lastError = error;
|
|
16227
|
-
// Don't retry authentication errors - they won't succeed on retry
|
|
16228
|
-
if (error instanceof AuthenticationError) {
|
|
16229
|
-
throw error;
|
|
16432
|
+
executeWithRetry(operation, shouldRetryStatus) {
|
|
16433
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16434
|
+
let lastError = null;
|
|
16435
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
16436
|
+
try {
|
|
16437
|
+
const response = yield operation();
|
|
16438
|
+
// Check if we should retry based on status code
|
|
16439
|
+
if (shouldRetryStatus(response.status) &&
|
|
16440
|
+
attempt < this.config.maxRetries) {
|
|
16441
|
+
const delay = this.calculateDelay(attempt, response);
|
|
16442
|
+
this.logger.warn(`Request failed with status ${response.status}, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries})`);
|
|
16443
|
+
yield sleep(delay);
|
|
16444
|
+
continue;
|
|
16445
|
+
}
|
|
16446
|
+
// Success or non-retryable status
|
|
16447
|
+
return response;
|
|
16230
16448
|
}
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16449
|
+
catch (error) {
|
|
16450
|
+
lastError = error;
|
|
16451
|
+
// Don't retry authentication errors - they won't succeed on retry
|
|
16452
|
+
if (error instanceof AuthenticationError) {
|
|
16453
|
+
throw error;
|
|
16454
|
+
}
|
|
16455
|
+
// Retry on network errors
|
|
16456
|
+
if (attempt < this.config.maxRetries) {
|
|
16457
|
+
const delay = this.calculateDelay(attempt);
|
|
16458
|
+
this.logger.warn(`Network error, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries}):`, error);
|
|
16459
|
+
yield sleep(delay);
|
|
16460
|
+
continue;
|
|
16461
|
+
}
|
|
16237
16462
|
}
|
|
16238
16463
|
}
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16464
|
+
// All retries exhausted
|
|
16465
|
+
throw lastError || new Error('Request failed after all retry attempts');
|
|
16466
|
+
});
|
|
16242
16467
|
}
|
|
16243
16468
|
/**
|
|
16244
16469
|
* Calculate retry delay with exponential backoff and jitter
|
|
@@ -16246,8 +16471,8 @@ class RetryHandler {
|
|
|
16246
16471
|
calculateDelay(attempt, response) {
|
|
16247
16472
|
var _a, _b;
|
|
16248
16473
|
// Check for Retry-After header
|
|
16249
|
-
|
|
16250
|
-
|
|
16474
|
+
const retryAfter = (_b = (_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, 'Retry-After');
|
|
16475
|
+
if (retryAfter) {
|
|
16251
16476
|
const retryAfterSeconds = parseInt(retryAfter, 10);
|
|
16252
16477
|
if (!isNaN(retryAfterSeconds)) {
|
|
16253
16478
|
return Math.min(retryAfterSeconds * 1000, this.config.maxDelay);
|
|
@@ -16268,26 +16493,34 @@ class RetryHandler {
|
|
|
16268
16493
|
/**
|
|
16269
16494
|
* Process offline queue in background
|
|
16270
16495
|
*/
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
|
|
16276
|
-
|
|
16277
|
-
|
|
16496
|
+
function processQueueInBackground(offlineConfig, retryConfig, auth, logger) {
|
|
16497
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16498
|
+
if (!offlineConfig.enabled) {
|
|
16499
|
+
return;
|
|
16500
|
+
}
|
|
16501
|
+
const queue = new OfflineQueue(offlineConfig, logger);
|
|
16502
|
+
const authHeaders = generateAuthHeaders(auth);
|
|
16503
|
+
queue
|
|
16504
|
+
.processWithAuth(retryConfig.retryOn, authHeaders)
|
|
16505
|
+
.catch((error) => {
|
|
16506
|
+
logger.warn('Failed to process offline queue:', error);
|
|
16507
|
+
});
|
|
16278
16508
|
});
|
|
16279
16509
|
}
|
|
16280
16510
|
/**
|
|
16281
16511
|
* Handle offline failure by queueing request
|
|
16512
|
+
* SECURITY: Does not pass auth headers to queue - they will be regenerated when processing
|
|
16282
16513
|
*/
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16514
|
+
function handleOfflineFailure(error, endpoint, body, contentHeaders, _auth, offlineConfig, logger) {
|
|
16515
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16516
|
+
if (!offlineConfig.enabled || !isNetworkError(error)) {
|
|
16517
|
+
return;
|
|
16518
|
+
}
|
|
16519
|
+
logger.warn('Network error detected, queueing request for offline retry');
|
|
16520
|
+
const queue = new OfflineQueue(offlineConfig, logger);
|
|
16521
|
+
// SECURITY: Only pass content headers, not auth headers - auth will be regenerated when processing
|
|
16522
|
+
yield queue.enqueue(endpoint, body, contentHeaders);
|
|
16523
|
+
});
|
|
16291
16524
|
}
|
|
16292
16525
|
// ============================================================================
|
|
16293
16526
|
// PUBLIC API
|
|
@@ -16309,22 +16542,28 @@ function getAuthHeaders(auth) {
|
|
|
16309
16542
|
* @param authOrOptions - Auth config or TransportOptions
|
|
16310
16543
|
* @returns Response from the server
|
|
16311
16544
|
*/
|
|
16312
|
-
|
|
16313
|
-
|
|
16314
|
-
|
|
16315
|
-
|
|
16316
|
-
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
|
|
16321
|
-
|
|
16322
|
-
|
|
16323
|
-
|
|
16324
|
-
|
|
16325
|
-
|
|
16326
|
-
|
|
16327
|
-
|
|
16545
|
+
function submitWithAuth(endpoint_1, body_1) {
|
|
16546
|
+
return __awaiter$1(this, arguments, void 0, function* (endpoint, body, contentHeaders = {}, options) {
|
|
16547
|
+
const logger = options.logger || getLogger();
|
|
16548
|
+
const retryConfig = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), options.retry);
|
|
16549
|
+
const offlineConfig = Object.assign(Object.assign({}, DEFAULT_OFFLINE_CONFIG), options.offline);
|
|
16550
|
+
// Security: Enforce HTTPS
|
|
16551
|
+
if (!isSecureEndpoint(endpoint)) {
|
|
16552
|
+
throw new InsecureEndpointError(endpoint);
|
|
16553
|
+
}
|
|
16554
|
+
// Process offline queue on each request (run in background without awaiting)
|
|
16555
|
+
processQueueInBackground(offlineConfig, retryConfig, options.auth, logger);
|
|
16556
|
+
try {
|
|
16557
|
+
// Send with retry logic
|
|
16558
|
+
const response = yield sendWithRetry(endpoint, body, contentHeaders, options.auth, retryConfig, logger);
|
|
16559
|
+
return response;
|
|
16560
|
+
}
|
|
16561
|
+
catch (error) {
|
|
16562
|
+
// Queue for offline retry if enabled
|
|
16563
|
+
yield handleOfflineFailure(error, endpoint, body, contentHeaders, options.auth, offlineConfig, logger);
|
|
16564
|
+
throw error;
|
|
16565
|
+
}
|
|
16566
|
+
});
|
|
16328
16567
|
}
|
|
16329
16568
|
// ============================================================================
|
|
16330
16569
|
// INTERNAL HELPERS
|
|
@@ -16332,21 +16571,28 @@ async function submitWithAuth(endpoint, body, contentHeaders = {}, options) {
|
|
|
16332
16571
|
/**
|
|
16333
16572
|
* Make HTTP request with auth headers
|
|
16334
16573
|
*/
|
|
16335
|
-
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
|
|
16339
|
-
|
|
16340
|
-
|
|
16341
|
-
|
|
16574
|
+
function makeRequest(endpoint, body, contentHeaders, auth) {
|
|
16575
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16576
|
+
if (!isSecureEndpoint(endpoint)) {
|
|
16577
|
+
throw new InsecureEndpointError(endpoint);
|
|
16578
|
+
}
|
|
16579
|
+
const authHeaders = generateAuthHeaders(auth);
|
|
16580
|
+
const headers = Object.assign(Object.assign({}, contentHeaders), authHeaders);
|
|
16581
|
+
return fetch(endpoint, {
|
|
16582
|
+
method: 'POST',
|
|
16583
|
+
headers,
|
|
16584
|
+
body,
|
|
16585
|
+
});
|
|
16342
16586
|
});
|
|
16343
16587
|
}
|
|
16344
16588
|
/**
|
|
16345
16589
|
* Send request with exponential backoff retry
|
|
16346
16590
|
*/
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16591
|
+
function sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger) {
|
|
16592
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16593
|
+
const retryHandler = new RetryHandler(retryConfig, logger);
|
|
16594
|
+
return retryHandler.executeWithRetry(() => __awaiter$1(this, void 0, void 0, function* () { return makeRequest(endpoint, body, contentHeaders, auth); }), (status) => retryConfig.retryOn.includes(status));
|
|
16595
|
+
});
|
|
16350
16596
|
}
|
|
16351
16597
|
/**
|
|
16352
16598
|
* Sleep for specified milliseconds
|
|
@@ -16480,17 +16726,19 @@ class BugReporter {
|
|
|
16480
16726
|
/**
|
|
16481
16727
|
* Submit a bug report with file uploads via presigned URLs
|
|
16482
16728
|
*/
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16729
|
+
submit(payload) {
|
|
16730
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16731
|
+
const dedupContext = this.validateAndExtractErrors(payload);
|
|
16732
|
+
try {
|
|
16733
|
+
logger$1.debug(`Submitting bug report to ${this.config.endpoint}`);
|
|
16734
|
+
const bugReportData = yield this.createBugReport(payload);
|
|
16735
|
+
yield this.handleFileUploads(bugReportData, payload, dedupContext);
|
|
16736
|
+
}
|
|
16737
|
+
finally {
|
|
16738
|
+
// Mark this specific report submission as complete
|
|
16739
|
+
this.deduplicator.markComplete(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
|
|
16740
|
+
}
|
|
16741
|
+
});
|
|
16494
16742
|
}
|
|
16495
16743
|
/**
|
|
16496
16744
|
* Validate submission and extract deduplication context
|
|
@@ -16519,81 +16767,85 @@ class BugReporter {
|
|
|
16519
16767
|
* Create bug report on server and get bug ID with presigned URLs
|
|
16520
16768
|
* @private
|
|
16521
16769
|
*/
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
-
|
|
16526
|
-
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
|
|
16532
|
-
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
|
|
16555
|
-
|
|
16770
|
+
createBugReport(payload) {
|
|
16771
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16772
|
+
const { report } = payload, metadata = __rest$1(payload, ["report"]);
|
|
16773
|
+
const fileAnalysis = analyzeReportFiles(report);
|
|
16774
|
+
logger$1.debug('File upload detection', fileAnalysis);
|
|
16775
|
+
const createPayload = Object.assign(Object.assign({}, metadata), { report: {
|
|
16776
|
+
console: report.console,
|
|
16777
|
+
network: report.network,
|
|
16778
|
+
metadata: report.metadata,
|
|
16779
|
+
}, hasScreenshot: fileAnalysis.hasScreenshot, hasReplay: fileAnalysis.hasReplay });
|
|
16780
|
+
const response = yield submitWithAuth(this.config.endpoint, JSON.stringify(createPayload), { 'Content-Type': 'application/json' }, {
|
|
16781
|
+
auth: this.config.auth,
|
|
16782
|
+
retry: this.config.retry,
|
|
16783
|
+
offline: this.config.offline,
|
|
16784
|
+
});
|
|
16785
|
+
if (!response.ok) {
|
|
16786
|
+
const errorText = yield response.text().catch(() => 'Unknown error');
|
|
16787
|
+
throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
|
|
16788
|
+
}
|
|
16789
|
+
const result = yield response.json();
|
|
16790
|
+
if (!isBugReportResponse(result)) {
|
|
16791
|
+
throw new Error('Invalid server response format');
|
|
16792
|
+
}
|
|
16793
|
+
if (!result.success) {
|
|
16794
|
+
throw new Error('Bug report creation failed on server');
|
|
16795
|
+
}
|
|
16796
|
+
// TypeScript now knows result.success is true, so result.data exists (validated by type guard)
|
|
16797
|
+
const bugData = result.data;
|
|
16798
|
+
logger$1.debug('Bug report creation response', {
|
|
16799
|
+
success: result.success,
|
|
16800
|
+
bugId: bugData.id,
|
|
16801
|
+
hasPresignedUrls: !!bugData.presignedUrls,
|
|
16802
|
+
presignedUrlKeys: bugData.presignedUrls
|
|
16803
|
+
? Object.keys(bugData.presignedUrls)
|
|
16804
|
+
: [],
|
|
16805
|
+
});
|
|
16806
|
+
return bugData;
|
|
16556
16807
|
});
|
|
16557
|
-
return bugData;
|
|
16558
16808
|
}
|
|
16559
16809
|
/**
|
|
16560
16810
|
* Handle file uploads using presigned URLs
|
|
16561
16811
|
* @private
|
|
16562
16812
|
*/
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
|
|
16566
|
-
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16813
|
+
handleFileUploads(bugReportData, payload, dedupContext) {
|
|
16814
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16815
|
+
// bugReportData.id is guaranteed to exist by type guard validation in createBugReport
|
|
16816
|
+
const bugId = bugReportData.id;
|
|
16817
|
+
const { report } = payload;
|
|
16818
|
+
const fileAnalysis = analyzeReportFiles(report);
|
|
16819
|
+
if (!fileAnalysis.hasScreenshot && !fileAnalysis.hasReplay) {
|
|
16820
|
+
logger$1.debug('No files to upload, bug report created successfully', {
|
|
16821
|
+
bugId,
|
|
16822
|
+
});
|
|
16823
|
+
this.deduplicator.recordSubmission(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
|
|
16824
|
+
return;
|
|
16825
|
+
}
|
|
16826
|
+
if (!bugReportData.presignedUrls) {
|
|
16827
|
+
logger$1.error('Presigned URLs not returned despite requesting file uploads', {
|
|
16828
|
+
bugId,
|
|
16829
|
+
hasScreenshot: fileAnalysis.hasScreenshot,
|
|
16830
|
+
hasReplay: fileAnalysis.hasReplay,
|
|
16831
|
+
});
|
|
16832
|
+
throw new Error('Server did not provide presigned URLs for file uploads. Check backend configuration.');
|
|
16833
|
+
}
|
|
16834
|
+
const apiEndpoint = getApiBaseUrl(this.config.endpoint);
|
|
16835
|
+
const uploadHandler = new FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
|
|
16836
|
+
try {
|
|
16837
|
+
yield uploadHandler.uploadFiles(bugId, report, bugReportData.presignedUrls);
|
|
16838
|
+
logger$1.debug('File uploads completed successfully', { bugId });
|
|
16839
|
+
this.deduplicator.recordSubmission(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
|
|
16840
|
+
}
|
|
16841
|
+
catch (error) {
|
|
16842
|
+
logger$1.error('File upload failed', {
|
|
16843
|
+
bugId,
|
|
16844
|
+
error: formatSubmissionError('Upload', error),
|
|
16845
|
+
});
|
|
16846
|
+
throw new Error(formatSubmissionError(`Bug report created (ID: ${bugId}) but file upload failed`, error));
|
|
16847
|
+
}
|
|
16848
|
+
});
|
|
16597
16849
|
}
|
|
16598
16850
|
/**
|
|
16599
16851
|
* Cleanup resources
|
|
@@ -16621,8 +16873,10 @@ class DirectUploader {
|
|
|
16621
16873
|
* @param onProgress - Optional progress callback
|
|
16622
16874
|
* @returns Upload result with storage key
|
|
16623
16875
|
*/
|
|
16624
|
-
|
|
16625
|
-
return this
|
|
16876
|
+
uploadScreenshot(file, onProgress) {
|
|
16877
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16878
|
+
return this.uploadFile(file, 'screenshot', 'screenshot.png', onProgress);
|
|
16879
|
+
});
|
|
16626
16880
|
}
|
|
16627
16881
|
/**
|
|
16628
16882
|
* Upload a compressed session replay directly to storage
|
|
@@ -16630,8 +16884,10 @@ class DirectUploader {
|
|
|
16630
16884
|
* @param onProgress - Optional progress callback
|
|
16631
16885
|
* @returns Upload result with storage key
|
|
16632
16886
|
*/
|
|
16633
|
-
|
|
16634
|
-
return this
|
|
16887
|
+
uploadReplay(compressedData, onProgress) {
|
|
16888
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16889
|
+
return this.uploadFile(compressedData, 'replay', 'replay.gz', onProgress);
|
|
16890
|
+
});
|
|
16635
16891
|
}
|
|
16636
16892
|
/**
|
|
16637
16893
|
* Upload an attachment file directly to storage
|
|
@@ -16639,8 +16895,10 @@ class DirectUploader {
|
|
|
16639
16895
|
* @param onProgress - Optional progress callback
|
|
16640
16896
|
* @returns Upload result with storage key
|
|
16641
16897
|
*/
|
|
16642
|
-
|
|
16643
|
-
return this
|
|
16898
|
+
uploadAttachment(file, onProgress) {
|
|
16899
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16900
|
+
return this.uploadFile(file, 'attachment', file.name, onProgress);
|
|
16901
|
+
});
|
|
16644
16902
|
}
|
|
16645
16903
|
/**
|
|
16646
16904
|
* Generic file upload method
|
|
@@ -16648,121 +16906,127 @@ class DirectUploader {
|
|
|
16648
16906
|
* 2. Upload file directly to storage using presigned URL
|
|
16649
16907
|
* 3. Confirm upload with API
|
|
16650
16908
|
*/
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16909
|
+
uploadFile(file, fileType, filename, onProgress) {
|
|
16910
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16911
|
+
try {
|
|
16912
|
+
// Step 1: Get presigned upload URL
|
|
16913
|
+
const presignedUrlResponse = yield this.requestPresignedUrl(fileType, filename);
|
|
16914
|
+
if (!presignedUrlResponse.success) {
|
|
16915
|
+
return {
|
|
16916
|
+
success: false,
|
|
16917
|
+
error: presignedUrlResponse.error || 'Failed to get presigned URL',
|
|
16918
|
+
};
|
|
16919
|
+
}
|
|
16920
|
+
const { uploadUrl, storageKey } = presignedUrlResponse.data;
|
|
16921
|
+
// Step 2: Upload file to storage using presigned URL
|
|
16922
|
+
const uploadSuccess = yield this.uploadToStorage(uploadUrl, file, onProgress);
|
|
16923
|
+
if (!uploadSuccess) {
|
|
16924
|
+
return {
|
|
16925
|
+
success: false,
|
|
16926
|
+
error: 'Failed to upload file to storage',
|
|
16927
|
+
};
|
|
16928
|
+
}
|
|
16929
|
+
// Step 3: Confirm upload with API
|
|
16930
|
+
const confirmSuccess = yield this.confirmUpload(fileType);
|
|
16931
|
+
if (!confirmSuccess) {
|
|
16932
|
+
return {
|
|
16933
|
+
success: false,
|
|
16934
|
+
error: 'Failed to confirm upload',
|
|
16935
|
+
};
|
|
16936
|
+
}
|
|
16665
16937
|
return {
|
|
16666
|
-
success:
|
|
16667
|
-
|
|
16938
|
+
success: true,
|
|
16939
|
+
storageKey,
|
|
16668
16940
|
};
|
|
16669
16941
|
}
|
|
16670
|
-
|
|
16671
|
-
const confirmSuccess = await this.confirmUpload(fileType);
|
|
16672
|
-
if (!confirmSuccess) {
|
|
16942
|
+
catch (error) {
|
|
16673
16943
|
return {
|
|
16674
16944
|
success: false,
|
|
16675
|
-
error:
|
|
16945
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
16676
16946
|
};
|
|
16677
16947
|
}
|
|
16678
|
-
|
|
16679
|
-
success: true,
|
|
16680
|
-
storageKey,
|
|
16681
|
-
};
|
|
16682
|
-
}
|
|
16683
|
-
catch (error) {
|
|
16684
|
-
return {
|
|
16685
|
-
success: false,
|
|
16686
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
16687
|
-
};
|
|
16688
|
-
}
|
|
16948
|
+
});
|
|
16689
16949
|
}
|
|
16690
16950
|
/**
|
|
16691
16951
|
* Request a presigned URL from the API
|
|
16692
16952
|
*/
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
16708
|
-
|
|
16709
|
-
|
|
16953
|
+
requestPresignedUrl(fileType, filename) {
|
|
16954
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16955
|
+
try {
|
|
16956
|
+
const response = yield fetch(`${this.config.apiEndpoint}/api/v1/uploads/presigned-url`, {
|
|
16957
|
+
method: 'POST',
|
|
16958
|
+
headers: {
|
|
16959
|
+
'Content-Type': 'application/json',
|
|
16960
|
+
'x-api-key': this.config.apiKey,
|
|
16961
|
+
},
|
|
16962
|
+
body: JSON.stringify({
|
|
16963
|
+
projectId: this.config.projectId,
|
|
16964
|
+
bugId: this.config.bugId,
|
|
16965
|
+
fileType,
|
|
16966
|
+
filename,
|
|
16967
|
+
}),
|
|
16968
|
+
});
|
|
16969
|
+
if (!response.ok) {
|
|
16970
|
+
const errorText = yield response.text();
|
|
16971
|
+
return {
|
|
16972
|
+
success: false,
|
|
16973
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
16974
|
+
};
|
|
16975
|
+
}
|
|
16976
|
+
const result = yield response.json();
|
|
16977
|
+
return {
|
|
16978
|
+
success: result.success,
|
|
16979
|
+
data: result.data,
|
|
16980
|
+
error: result.error,
|
|
16981
|
+
};
|
|
16982
|
+
}
|
|
16983
|
+
catch (error) {
|
|
16710
16984
|
return {
|
|
16711
16985
|
success: false,
|
|
16712
|
-
error:
|
|
16986
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
16713
16987
|
};
|
|
16714
16988
|
}
|
|
16715
|
-
|
|
16716
|
-
return {
|
|
16717
|
-
success: result.success,
|
|
16718
|
-
data: result.data,
|
|
16719
|
-
error: result.error,
|
|
16720
|
-
};
|
|
16721
|
-
}
|
|
16722
|
-
catch (error) {
|
|
16723
|
-
return {
|
|
16724
|
-
success: false,
|
|
16725
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
16726
|
-
};
|
|
16727
|
-
}
|
|
16989
|
+
});
|
|
16728
16990
|
}
|
|
16729
16991
|
/**
|
|
16730
16992
|
* Upload file to storage using presigned URL
|
|
16731
16993
|
* Uses XMLHttpRequest for progress tracking
|
|
16732
16994
|
*/
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16995
|
+
uploadToStorage(uploadUrl, file, onProgress) {
|
|
16996
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
16997
|
+
// Convert File/Blob to ArrayBuffer to prevent browser from auto-setting Content-Type header
|
|
16998
|
+
// This is critical for CORS compatibility with B2/S3 presigned URLs
|
|
16999
|
+
const arrayBuffer = yield this.fileToArrayBuffer(file);
|
|
17000
|
+
return new Promise((resolve) => {
|
|
17001
|
+
const xhr = new XMLHttpRequest();
|
|
17002
|
+
// Track upload progress
|
|
17003
|
+
if (onProgress) {
|
|
17004
|
+
xhr.upload.addEventListener('progress', (event) => {
|
|
17005
|
+
if (event.lengthComputable) {
|
|
17006
|
+
onProgress({
|
|
17007
|
+
loaded: event.loaded,
|
|
17008
|
+
total: event.total,
|
|
17009
|
+
percentage: Math.round((event.loaded / event.total) * 100),
|
|
17010
|
+
});
|
|
17011
|
+
}
|
|
17012
|
+
});
|
|
17013
|
+
}
|
|
17014
|
+
// Handle completion
|
|
17015
|
+
xhr.addEventListener('load', () => {
|
|
17016
|
+
resolve(xhr.status >= 200 && xhr.status < 300);
|
|
16749
17017
|
});
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
17018
|
+
// Handle errors
|
|
17019
|
+
xhr.addEventListener('error', () => {
|
|
17020
|
+
resolve(false);
|
|
17021
|
+
});
|
|
17022
|
+
xhr.addEventListener('abort', () => {
|
|
17023
|
+
resolve(false);
|
|
17024
|
+
});
|
|
17025
|
+
// Send file as ArrayBuffer (no Content-Type header)
|
|
17026
|
+
// The presigned URL signature does NOT include Content-Type
|
|
17027
|
+
xhr.open('PUT', uploadUrl);
|
|
17028
|
+
xhr.send(arrayBuffer);
|
|
16761
17029
|
});
|
|
16762
|
-
// Send file as ArrayBuffer (no Content-Type header)
|
|
16763
|
-
// The presigned URL signature does NOT include Content-Type
|
|
16764
|
-
xhr.open('PUT', uploadUrl);
|
|
16765
|
-
xhr.send(arrayBuffer);
|
|
16766
17030
|
});
|
|
16767
17031
|
}
|
|
16768
17032
|
/**
|
|
@@ -16783,23 +17047,25 @@ class DirectUploader {
|
|
|
16783
17047
|
/**
|
|
16784
17048
|
* Confirm successful upload with the API
|
|
16785
17049
|
*/
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
17050
|
+
confirmUpload(fileType) {
|
|
17051
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17052
|
+
try {
|
|
17053
|
+
const response = yield fetch(`${this.config.apiEndpoint}/api/v1/reports/${this.config.bugId}/confirm-upload`, {
|
|
17054
|
+
method: 'POST',
|
|
17055
|
+
headers: {
|
|
17056
|
+
'Content-Type': 'application/json',
|
|
17057
|
+
'x-api-key': this.config.apiKey,
|
|
17058
|
+
},
|
|
17059
|
+
body: JSON.stringify({
|
|
17060
|
+
fileType,
|
|
17061
|
+
}),
|
|
17062
|
+
});
|
|
17063
|
+
return response.ok;
|
|
17064
|
+
}
|
|
17065
|
+
catch (_a) {
|
|
17066
|
+
return false;
|
|
17067
|
+
}
|
|
17068
|
+
});
|
|
16803
17069
|
}
|
|
16804
17070
|
}
|
|
16805
17071
|
|
|
@@ -16813,30 +17079,32 @@ class DirectUploader {
|
|
|
16813
17079
|
* @param events - Session replay events array
|
|
16814
17080
|
* @returns Compressed Blob (gzip)
|
|
16815
17081
|
*/
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
17082
|
+
function compressReplayEvents(events) {
|
|
17083
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17084
|
+
// Convert events to JSON string
|
|
17085
|
+
const jsonString = JSON.stringify(events);
|
|
17086
|
+
const textEncoder = new TextEncoder();
|
|
17087
|
+
const data = textEncoder.encode(jsonString);
|
|
17088
|
+
// Check if CompressionStream is supported (Chrome 80+, Firefox 113+, Safari 16.4+)
|
|
17089
|
+
if (typeof CompressionStream === 'undefined') {
|
|
17090
|
+
console.warn('CompressionStream not supported, uploading uncompressed replay data');
|
|
17091
|
+
return new Blob([data], { type: 'application/json' });
|
|
17092
|
+
}
|
|
17093
|
+
try {
|
|
17094
|
+
// Use modern streaming API: Blob → ReadableStream → CompressionStream → Response → Blob
|
|
17095
|
+
const blob = new Blob([data]);
|
|
17096
|
+
const compressedStream = blob
|
|
17097
|
+
.stream()
|
|
17098
|
+
.pipeThrough(new CompressionStream('gzip'));
|
|
17099
|
+
return yield new Response(compressedStream, {
|
|
17100
|
+
headers: { 'Content-Type': 'application/gzip' },
|
|
17101
|
+
}).blob();
|
|
17102
|
+
}
|
|
17103
|
+
catch (error) {
|
|
17104
|
+
console.error('Compression failed, uploading uncompressed:', error);
|
|
17105
|
+
return new Blob([data], { type: 'application/json' });
|
|
17106
|
+
}
|
|
17107
|
+
});
|
|
16840
17108
|
}
|
|
16841
17109
|
/**
|
|
16842
17110
|
* Convert screenshot canvas to Blob
|
|
@@ -16844,16 +17112,18 @@ async function compressReplayEvents(events) {
|
|
|
16844
17112
|
* @param quality - JPEG quality (0-1), default 0.9
|
|
16845
17113
|
* @returns Screenshot Blob
|
|
16846
17114
|
*/
|
|
16847
|
-
|
|
16848
|
-
return
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
16855
|
-
|
|
16856
|
-
|
|
17115
|
+
function canvasToBlob(canvas_1) {
|
|
17116
|
+
return __awaiter$1(this, arguments, void 0, function* (canvas, quality = 0.9) {
|
|
17117
|
+
return new Promise((resolve, reject) => {
|
|
17118
|
+
canvas.toBlob((blob) => {
|
|
17119
|
+
if (blob) {
|
|
17120
|
+
resolve(blob);
|
|
17121
|
+
}
|
|
17122
|
+
else {
|
|
17123
|
+
reject(new Error('Failed to convert canvas to Blob'));
|
|
17124
|
+
}
|
|
17125
|
+
}, 'image/png', quality);
|
|
17126
|
+
});
|
|
16857
17127
|
});
|
|
16858
17128
|
}
|
|
16859
17129
|
/**
|
|
@@ -16910,51 +17180,53 @@ function mergeReplayConfig(userConfig, backendSettings) {
|
|
|
16910
17180
|
* @param apiKey - Optional API key for authentication
|
|
16911
17181
|
* @returns Replay quality settings with defaults applied
|
|
16912
17182
|
*/
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
|
|
16928
|
-
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
16933
|
-
|
|
16934
|
-
|
|
16935
|
-
|
|
16936
|
-
|
|
17183
|
+
function fetchReplaySettings(endpoint, apiKey) {
|
|
17184
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17185
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
17186
|
+
const defaults = {
|
|
17187
|
+
duration: DEFAULT_REPLAY_DURATION_SECONDS,
|
|
17188
|
+
inline_stylesheets: true,
|
|
17189
|
+
inline_images: false,
|
|
17190
|
+
collect_fonts: true,
|
|
17191
|
+
record_canvas: false,
|
|
17192
|
+
record_cross_origin_iframes: false,
|
|
17193
|
+
sampling_mousemove: DEFAULT_MOUSEMOVE_SAMPLING,
|
|
17194
|
+
sampling_scroll: DEFAULT_SCROLL_SAMPLING,
|
|
17195
|
+
};
|
|
17196
|
+
try {
|
|
17197
|
+
const apiBaseUrl = getApiBaseUrl(endpoint);
|
|
17198
|
+
const headers = {};
|
|
17199
|
+
if (apiKey) {
|
|
17200
|
+
headers['x-api-key'] = apiKey;
|
|
17201
|
+
}
|
|
17202
|
+
const response = yield fetch(`${apiBaseUrl}/api/v1/settings/replay`, {
|
|
17203
|
+
headers,
|
|
17204
|
+
});
|
|
17205
|
+
if (!response.ok) {
|
|
17206
|
+
logger.warn(`Failed to fetch replay settings: ${response.status}. Using defaults.`);
|
|
17207
|
+
return defaults;
|
|
17208
|
+
}
|
|
17209
|
+
const result = yield response.json();
|
|
17210
|
+
if (!result.success || !result.data) {
|
|
17211
|
+
logger.warn('Invalid replay settings response. Using defaults.');
|
|
17212
|
+
return defaults;
|
|
17213
|
+
}
|
|
17214
|
+
return {
|
|
17215
|
+
duration: (_a = result.data.duration) !== null && _a !== void 0 ? _a : defaults.duration,
|
|
17216
|
+
inline_stylesheets: (_b = result.data.inline_stylesheets) !== null && _b !== void 0 ? _b : defaults.inline_stylesheets,
|
|
17217
|
+
inline_images: (_c = result.data.inline_images) !== null && _c !== void 0 ? _c : defaults.inline_images,
|
|
17218
|
+
collect_fonts: (_d = result.data.collect_fonts) !== null && _d !== void 0 ? _d : defaults.collect_fonts,
|
|
17219
|
+
record_canvas: (_e = result.data.record_canvas) !== null && _e !== void 0 ? _e : defaults.record_canvas,
|
|
17220
|
+
record_cross_origin_iframes: (_f = result.data.record_cross_origin_iframes) !== null && _f !== void 0 ? _f : defaults.record_cross_origin_iframes,
|
|
17221
|
+
sampling_mousemove: (_g = result.data.sampling_mousemove) !== null && _g !== void 0 ? _g : defaults.sampling_mousemove,
|
|
17222
|
+
sampling_scroll: (_h = result.data.sampling_scroll) !== null && _h !== void 0 ? _h : defaults.sampling_scroll,
|
|
17223
|
+
};
|
|
16937
17224
|
}
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
logger.warn('Invalid replay settings response. Using defaults.');
|
|
17225
|
+
catch (error) {
|
|
17226
|
+
logger.warn('Failed to fetch replay settings from backend. Using defaults.', error);
|
|
16941
17227
|
return defaults;
|
|
16942
17228
|
}
|
|
16943
|
-
|
|
16944
|
-
duration: (_a = result.data.duration) !== null && _a !== void 0 ? _a : defaults.duration,
|
|
16945
|
-
inline_stylesheets: (_b = result.data.inline_stylesheets) !== null && _b !== void 0 ? _b : defaults.inline_stylesheets,
|
|
16946
|
-
inline_images: (_c = result.data.inline_images) !== null && _c !== void 0 ? _c : defaults.inline_images,
|
|
16947
|
-
collect_fonts: (_d = result.data.collect_fonts) !== null && _d !== void 0 ? _d : defaults.collect_fonts,
|
|
16948
|
-
record_canvas: (_e = result.data.record_canvas) !== null && _e !== void 0 ? _e : defaults.record_canvas,
|
|
16949
|
-
record_cross_origin_iframes: (_f = result.data.record_cross_origin_iframes) !== null && _f !== void 0 ? _f : defaults.record_cross_origin_iframes,
|
|
16950
|
-
sampling_mousemove: (_g = result.data.sampling_mousemove) !== null && _g !== void 0 ? _g : defaults.sampling_mousemove,
|
|
16951
|
-
sampling_scroll: (_h = result.data.sampling_scroll) !== null && _h !== void 0 ? _h : defaults.sampling_scroll,
|
|
16952
|
-
};
|
|
16953
|
-
}
|
|
16954
|
-
catch (error) {
|
|
16955
|
-
logger.warn('Failed to fetch replay settings from backend. Using defaults.', error);
|
|
16956
|
-
return defaults;
|
|
16957
|
-
}
|
|
17229
|
+
});
|
|
16958
17230
|
}
|
|
16959
17231
|
class BugSpotter {
|
|
16960
17232
|
constructor(config) {
|
|
@@ -16982,55 +17254,59 @@ class BugSpotter {
|
|
|
16982
17254
|
const widgetEnabled = (_f = config.showWidget) !== null && _f !== void 0 ? _f : true;
|
|
16983
17255
|
if (widgetEnabled) {
|
|
16984
17256
|
this.widget = new FloatingButton(config.widgetOptions);
|
|
16985
|
-
this.widget.onClick(
|
|
16986
|
-
|
|
16987
|
-
});
|
|
16988
|
-
}
|
|
16989
|
-
}
|
|
16990
|
-
static
|
|
16991
|
-
|
|
16992
|
-
|
|
16993
|
-
|
|
16994
|
-
'
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
|
|
17257
|
+
this.widget.onClick(() => __awaiter$1(this, void 0, void 0, function* () {
|
|
17258
|
+
yield this.handleBugReport();
|
|
17259
|
+
}));
|
|
17260
|
+
}
|
|
17261
|
+
}
|
|
17262
|
+
static init(config) {
|
|
17263
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17264
|
+
// If instance exists, warn about singleton behavior
|
|
17265
|
+
if (BugSpotter.instance) {
|
|
17266
|
+
logger.warn('BugSpotter.init() called multiple times. Returning existing instance. ' +
|
|
17267
|
+
'Call destroy() first to reinitialize with new config.');
|
|
17268
|
+
return BugSpotter.instance;
|
|
17269
|
+
}
|
|
17270
|
+
// If initialization is already in progress, wait for it
|
|
17271
|
+
if (BugSpotter.initPromise) {
|
|
17272
|
+
logger.warn('BugSpotter.init() called while initialization in progress. Waiting...');
|
|
17273
|
+
return BugSpotter.initPromise;
|
|
17274
|
+
}
|
|
17275
|
+
// Start initialization and cache the promise
|
|
17276
|
+
BugSpotter.initPromise = BugSpotter.createInstance(config);
|
|
17277
|
+
try {
|
|
17278
|
+
BugSpotter.instance = yield BugSpotter.initPromise;
|
|
17279
|
+
return BugSpotter.instance;
|
|
17280
|
+
}
|
|
17281
|
+
finally {
|
|
17282
|
+
// Clear the promise once initialization completes (success or failure)
|
|
17283
|
+
BugSpotter.initPromise = undefined;
|
|
17284
|
+
}
|
|
17285
|
+
});
|
|
17012
17286
|
}
|
|
17013
17287
|
/**
|
|
17014
17288
|
* Internal factory method to create a new BugSpotter instance
|
|
17015
17289
|
* Fetches replay settings from backend before initialization
|
|
17016
17290
|
*/
|
|
17017
|
-
static
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17291
|
+
static createInstance(config) {
|
|
17292
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17293
|
+
var _a, _b, _c;
|
|
17294
|
+
// Fetch replay quality settings from backend if replay is enabled
|
|
17295
|
+
let backendSettings = null;
|
|
17296
|
+
const replayEnabled = (_b = (_a = config.replay) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
|
|
17297
|
+
if (replayEnabled && config.endpoint) {
|
|
17298
|
+
// Validate auth is configured before attempting fetch
|
|
17299
|
+
if (!((_c = config.auth) === null || _c === void 0 ? void 0 : _c.apiKey)) {
|
|
17300
|
+
logger.warn('Endpoint provided but no API key configured. Skipping backend settings fetch.');
|
|
17301
|
+
}
|
|
17302
|
+
else {
|
|
17303
|
+
backendSettings = yield fetchReplaySettings(config.endpoint, config.auth.apiKey);
|
|
17304
|
+
}
|
|
17029
17305
|
}
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17306
|
+
// Merge backend settings with user config (user config takes precedence)
|
|
17307
|
+
const mergedConfig = Object.assign(Object.assign({}, config), { replay: mergeReplayConfig(config.replay, backendSettings) });
|
|
17308
|
+
return new BugSpotter(mergedConfig);
|
|
17309
|
+
});
|
|
17034
17310
|
}
|
|
17035
17311
|
static getInstance() {
|
|
17036
17312
|
return BugSpotter.instance || null;
|
|
@@ -17040,40 +17316,46 @@ class BugSpotter {
|
|
|
17040
17316
|
* Note: Screenshot is captured for modal preview only (_screenshotPreview)
|
|
17041
17317
|
* File uploads use presigned URLs returned from the backend
|
|
17042
17318
|
*/
|
|
17043
|
-
|
|
17044
|
-
return
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17319
|
+
capture() {
|
|
17320
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17321
|
+
return yield this.captureManager.captureAll();
|
|
17322
|
+
});
|
|
17323
|
+
}
|
|
17324
|
+
handleBugReport() {
|
|
17325
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17326
|
+
const report = yield this.capture();
|
|
17327
|
+
const modal = new BugReportModal({
|
|
17328
|
+
onSubmit: (data) => __awaiter$1(this, void 0, void 0, function* () {
|
|
17329
|
+
logger.log('Submitting bug:', Object.assign(Object.assign({}, data), { report }));
|
|
17330
|
+
// Send to endpoint if configured
|
|
17331
|
+
if (this.config.endpoint) {
|
|
17332
|
+
try {
|
|
17333
|
+
yield this.submit(Object.assign(Object.assign({}, data), { report }));
|
|
17334
|
+
logger.log('Bug report submitted successfully');
|
|
17335
|
+
}
|
|
17336
|
+
catch (error) {
|
|
17337
|
+
logger.error('Failed to submit bug report:', error);
|
|
17338
|
+
// Re-throw to allow UI to handle errors if needed
|
|
17339
|
+
throw error;
|
|
17340
|
+
}
|
|
17061
17341
|
}
|
|
17062
|
-
}
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
}
|
|
17342
|
+
}),
|
|
17343
|
+
onProgress: (message) => {
|
|
17344
|
+
logger.debug('Upload progress:', message);
|
|
17345
|
+
},
|
|
17346
|
+
});
|
|
17347
|
+
modal.show(report._screenshotPreview || '');
|
|
17067
17348
|
});
|
|
17068
|
-
modal.show(report._screenshotPreview || '');
|
|
17069
17349
|
}
|
|
17070
17350
|
/**
|
|
17071
17351
|
* Submit a bug report with file uploads via presigned URLs
|
|
17072
17352
|
* @param payload - Bug report payload with title, description, and report data
|
|
17073
17353
|
* @public - Exposed for programmatic submission (bypassing modal)
|
|
17074
17354
|
*/
|
|
17075
|
-
|
|
17076
|
-
|
|
17355
|
+
submit(payload) {
|
|
17356
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
17357
|
+
yield this.bugReporter.submit(payload);
|
|
17358
|
+
});
|
|
17077
17359
|
}
|
|
17078
17360
|
getConfig() {
|
|
17079
17361
|
return Object.assign({}, this.config);
|
|
@@ -17110,5 +17392,5 @@ function sanitize(text) {
|
|
|
17110
17392
|
return sanitizer.sanitize(text);
|
|
17111
17393
|
}
|
|
17112
17394
|
|
|
17113
|
-
export { BugReportModal, BugSpotter, CircularBuffer, ConsoleCapture, DEFAULT_PATTERNS, DEFAULT_REPLAY_DURATION_SECONDS, DOMCollector, DirectUploader, FloatingButton, InvalidEndpointError, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, MetadataCapture, NetworkCapture, PATTERN_CATEGORIES, PATTERN_PRESETS, PatternBuilder, Sanitizer, ScreenshotCapture, VERSION, canvasToBlob, clearOfflineQueue, compressData, compressImage, compressReplayEvents, configureLogger, createLogger, createPatternConfig, createSanitizer, decompressData, estimateCompressedReplaySize, estimateSize, getApiBaseUrl, getAuthHeaders, getCompressionRatio, getLogger, getPattern, getPatternsByCategory, isWithinSizeLimit, sanitize, stripEndpointSuffix, submitWithAuth, validateAuthConfig, validatePattern };
|
|
17395
|
+
export { BugReportModal, BugSpotter, CircularBuffer, ConsoleCapture, DEFAULT_PATTERNS, DEFAULT_REPLAY_DURATION_SECONDS, DOMCollector, DirectUploader, FloatingButton, InvalidEndpointError, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, MetadataCapture, NetworkCapture, PATTERN_CATEGORIES, PATTERN_PRESETS, PatternBuilder, Sanitizer, ScreenshotCapture, VERSION, canvasToBlob, clearOfflineQueue, compressData, compressImage, compressReplayEvents, configureLogger, createLogger, createPatternConfig, createSanitizer, decompressData, BugSpotter as default, estimateCompressedReplaySize, estimateSize, getApiBaseUrl, getAuthHeaders, getCompressionRatio, getLogger, getPattern, getPatternsByCategory, isWithinSizeLimit, sanitize, stripEndpointSuffix, submitWithAuth, validateAuthConfig, validatePattern };
|
|
17114
17396
|
//# sourceMappingURL=index.esm.js.map
|