@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/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
- btn.innerHTML = this.options.customSvg;
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
- btn.innerHTML = DEFAULT_SVG_ICON;
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.innerHTML = DEFAULT_SVG_ICON;
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
- async mergeRedactions(originalDataUrl, redactionCanvas) {
1514
- return new Promise((resolve, reject) => {
1515
- const img = new Image();
1516
- img.onload = () => {
1517
- try {
1518
- const mergedCanvas = document.createElement('canvas');
1519
- mergedCanvas.width = img.naturalWidth || img.width;
1520
- mergedCanvas.height = img.naturalHeight || img.height;
1521
- const ctx = mergedCanvas.getContext('2d');
1522
- if (!ctx) {
1523
- reject(new Error('Failed to get canvas context'));
1524
- return;
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
- // Draw original image
1527
- ctx.drawImage(img, 0, 0);
1528
- // Draw redaction canvas on top
1529
- ctx.drawImage(redactionCanvas, 0, 0);
1530
- resolve(mergedCanvas.toDataURL());
1531
- }
1532
- catch (error) {
1533
- reject(error);
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
- async applyRedactions(imageDataUrl, redactions, redactionColor = '#000000') {
1546
- return new Promise((resolve, reject) => {
1547
- const img = new Image();
1548
- img.onload = () => {
1549
- try {
1550
- const canvas = document.createElement('canvas');
1551
- canvas.width = img.naturalWidth || img.width;
1552
- canvas.height = img.naturalHeight || img.height;
1553
- const ctx = canvas.getContext('2d');
1554
- if (!ctx) {
1555
- reject(new Error('Failed to get canvas context'));
1556
- return;
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
- // Draw original image
1559
- ctx.drawImage(img, 0, 0);
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
- resolve(canvas.toDataURL());
1566
- }
1567
- catch (error) {
1568
- reject(error);
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
- async resize(imageDataUrl, maxWidth, maxHeight) {
1581
- return new Promise((resolve, reject) => {
1582
- const img = new Image();
1583
- img.onload = () => {
1584
- try {
1585
- let width = img.naturalWidth || img.width;
1586
- let height = img.naturalHeight || img.height;
1587
- // Calculate new dimensions maintaining aspect ratio
1588
- if (width > maxWidth || height > maxHeight) {
1589
- const aspectRatio = width / height;
1590
- if (width > height) {
1591
- width = maxWidth;
1592
- height = width / aspectRatio;
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
- else {
1595
- height = maxHeight;
1596
- width = height * aspectRatio;
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
- const canvas = document.createElement('canvas');
1600
- canvas.width = width;
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
- ctx.drawImage(img, 0, 0, width, height);
1608
- resolve(canvas.toDataURL());
1609
- }
1610
- catch (error) {
1611
- reject(error);
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
- async convert(imageDataUrl, format, quality = 0.92) {
1624
- return new Promise((resolve, reject) => {
1625
- const img = new Image();
1626
- img.onload = () => {
1627
- try {
1628
- const canvas = document.createElement('canvas');
1629
- canvas.width = img.naturalWidth || img.width;
1630
- canvas.height = img.naturalHeight || img.height;
1631
- const ctx = canvas.getContext('2d');
1632
- if (!ctx) {
1633
- reject(new Error('Failed to get canvas context'));
1634
- return;
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
- ctx.drawImage(img, 0, 0);
1637
- resolve(canvas.toDataURL(format, quality));
1638
- }
1639
- catch (error) {
1640
- reject(error);
1641
- }
1642
- };
1643
- img.onerror = () => {
1644
- reject(new Error('Failed to load image'));
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
- async getDimensions(imageDataUrl) {
1653
- return new Promise((resolve, reject) => {
1654
- const img = new Image();
1655
- img.onload = () => {
1656
- resolve({
1657
- width: img.naturalWidth || img.width,
1658
- height: img.naturalHeight || img.height,
1659
- });
1660
- };
1661
- img.onerror = () => {
1662
- reject(new Error('Failed to load image'));
1663
- };
1664
- img.src = imageDataUrl;
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
- * PII Pattern Definitions
1693
- * Configurable regex patterns for detecting sensitive data (PII + credentials)
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
- * Default built-in patterns
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
- examples: [
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 (Visa, MC, Amex, Discover, etc.)',
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 (12 digits with date validation)',
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: 'International phone numbers',
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, etc.)',
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: 'Authentication tokens (Bearer, GitHub, JWT-like)',
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 (password=..., pwd:...)',
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
- * Get patterns sorted by priority
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
- return a.priority - b.priority;
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
- * Get pattern by name
2106
+ * Validate a custom pattern regex for performance issues
1816
2107
  */
1817
- function getPattern(name) {
1818
- return DEFAULT_PATTERNS[name];
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
- * Get patterns by category
2134
+ * URL helper utilities for endpoint validation and parsing.
1822
2135
  */
1823
- function getPatternsByCategory(category) {
1824
- return PATTERN_CATEGORIES[category].map((name) => {
1825
- return DEFAULT_PATTERNS[name];
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 all pattern names
2224
+ * Get patterns by category
1830
2225
  */
1831
- function getAllPatternNames() {
1832
- return Object.keys(DEFAULT_PATTERNS);
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$8 = getLogger();
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
- async handleSubmit(e) {
2498
- var _a;
2499
- e.preventDefault();
2500
- const elements = this.domCache.get();
2501
- // Prevent double submission
2502
- if (elements.submitButton.disabled) {
2503
- return;
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
- if (validation.errors.piiConfirmation) {
2531
- alert(validation.errors.piiConfirmation);
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
- return;
2534
- }
2535
- // Clear any previous error messages on successful validation
2536
- elements.titleError.style.display = 'none';
2537
- elements.descriptionError.style.display = 'none';
2538
- elements.submitError.style.display = 'none';
2539
- // Disable submit button and show loading state
2540
- const originalButtonText = elements.submitButton.textContent || 'Submit Bug Report';
2541
- elements.submitButton.disabled = true;
2542
- elements.submitButton.textContent = 'Preparing...';
2543
- elements.submitButton.classList.add('loading');
2544
- // Helper to update progress
2545
- const updateProgress = (message) => {
2546
- elements.submitButton.textContent = message;
2547
- elements.progressStatus.textContent = message; // Announce to screen readers
2548
- if (this.options.onProgress) {
2549
- this.options.onProgress(message);
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
- updateProgress('Preparing screenshot...');
2553
- // Prepare screenshot with redactions
2554
- let finalScreenshot = this.originalScreenshot;
2555
- if (this.redactionCanvas &&
2556
- this.redactionCanvas.getRedactions().length > 0) {
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
- finalScreenshot = await this.screenshotProcessor.mergeRedactions(this.originalScreenshot, this.redactionCanvas.getCanvas());
2891
+ updateProgress('Uploading report...');
2892
+ yield this.options.onSubmit(bugReportData);
2893
+ this.close();
2559
2894
  }
2560
- catch (mergeError) {
2561
- logger$8.error('Failed to merge redactions:', mergeError);
2562
- finalScreenshot = this.originalScreenshot;
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 = '0.3.1';
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
- async function compressData(data, config) {
10568
- var _a;
10569
- try {
10570
- const jsonString = dataToString(data);
10571
- const encoder = getTextEncoder();
10572
- const uint8Data = encoder.encode(jsonString);
10573
- const gzipLevel = (_a = config === null || config === void 0 ? void 0 : config.gzipLevel) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.GZIP_LEVEL;
10574
- // pako.gzip already returns Uint8Array, no need to wrap it
10575
- const compressed = pako.gzip(uint8Data, { level: gzipLevel });
10576
- return compressed;
10577
- }
10578
- catch (error) {
10579
- logger$5.error('Compression failed:', error);
10580
- throw error;
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
- async function compressImage(base64, config) {
10688
- var _a, _b, _c, _d;
10689
- try {
10690
- if (!isBrowserEnvironment()) {
10691
- return base64;
10692
- }
10693
- const maxWidth = (_a = config === null || config === void 0 ? void 0 : config.imageMaxWidth) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.IMAGE_MAX_WIDTH;
10694
- const maxHeight = (_b = config === null || config === void 0 ? void 0 : config.imageMaxHeight) !== null && _b !== void 0 ? _b : COMPRESSION_DEFAULTS.IMAGE_MAX_HEIGHT;
10695
- const webpQuality = (_c = config === null || config === void 0 ? void 0 : config.webpQuality) !== null && _c !== void 0 ? _c : COMPRESSION_DEFAULTS.IMAGE_WEBP_QUALITY;
10696
- const jpegQuality = (_d = config === null || config === void 0 ? void 0 : config.jpegQuality) !== null && _d !== void 0 ? _d : COMPRESSION_DEFAULTS.IMAGE_JPEG_QUALITY;
10697
- const timeout = COMPRESSION_DEFAULTS.IMAGE_LOAD_TIMEOUT;
10698
- const img = await loadImage(base64, timeout);
10699
- const canvas = document.createElement('canvas');
10700
- const ctx = canvas.getContext('2d');
10701
- if (!ctx) {
10702
- throw new Error('Failed to get 2D canvas context');
10703
- }
10704
- const { width, height } = calculateResizedDimensions(img.width, img.height, maxWidth, maxHeight);
10705
- canvas.width = width;
10706
- canvas.height = height;
10707
- // Enable high-quality image smoothing for better resize quality
10708
- ctx.imageSmoothingEnabled = true;
10709
- ctx.imageSmoothingQuality = 'high';
10710
- ctx.drawImage(img, 0, 0, width, height);
10711
- if (supportsWebP()) {
10712
- return canvas.toDataURL('image/webp', webpQuality);
10713
- }
10714
- else {
10715
- return canvas.toDataURL('image/jpeg', jpegQuality);
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
- catch (error) {
10719
- if ((config === null || config === void 0 ? void 0 : config.verbose) !== false) {
10720
- getLogger().error('Image compression failed:', error);
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
- return base64;
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
- async capture(targetElement) {
10778
- try {
10779
- const element = targetElement || this.options.targetElement || document.body;
10780
- const options = this.buildCaptureOptions();
10781
- const dataUrl = await toPng(element, options);
10782
- // Compress the screenshot to reduce payload size
10783
- // Converts to WebP if supported, resizes if too large
10784
- const compressed = await compressImage(dataUrl);
10785
- return compressed;
10786
- }
10787
- catch (error) {
10788
- this.handleError('capturing screenshot', error);
10789
- return this.getErrorPlaceholder();
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 = async (...args) => {
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 = await originalFetch(...args);
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$1(s, e) {
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$1(value, ["type"]);
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
- async captureAll() {
15443
- var _a, _b;
15444
- // Call synchronous methods directly
15445
- const consoleLogs = this.console.getLogs();
15446
- const networkRequests = this.network.getRequests();
15447
- const metadata = this.metadata.capture();
15448
- const replay = (_b = (_a = this.domCollector) === null || _a === void 0 ? void 0 : _a.getEvents()) !== null && _b !== void 0 ? _b : [];
15449
- // Await async screenshot capture
15450
- const screenshotPreview = await this.screenshot.capture();
15451
- return {
15452
- console: consoleLogs,
15453
- network: networkRequests,
15454
- metadata,
15455
- replay,
15456
- _screenshotPreview: screenshotPreview,
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
- async captureScreenshot() {
15463
- return await this.screenshot.capture();
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
- async uploadFiles(bugId, report, presignedUrls) {
15529
- const filesToUpload = await this.prepareFiles(report, presignedUrls);
15530
- if (filesToUpload.length === 0) {
15531
- return; // No files to upload
15532
- }
15533
- await this.uploadToStorage(filesToUpload);
15534
- await this.confirmUploads(filesToUpload, bugId);
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
- async prepareFiles(report, presignedUrls) {
15540
- const files = [];
15541
- // Prepare screenshot
15542
- if (report._screenshotPreview &&
15543
- report._screenshotPreview.startsWith('data:image/')) {
15544
- const screenshotUrl = this.getPresignedUrl('screenshot', presignedUrls);
15545
- const screenshotBlob = await this.dataUrlToBlob(report._screenshotPreview);
15546
- files.push({
15547
- type: 'screenshot',
15548
- url: screenshotUrl.uploadUrl,
15549
- key: screenshotUrl.storageKey,
15550
- blob: screenshotBlob,
15551
- });
15552
- }
15553
- // Prepare replay
15554
- if (report.replay && report.replay.length > 0) {
15555
- const replayUrl = this.getPresignedUrl('replay', presignedUrls);
15556
- const compressed = await compressData(report.replay);
15557
- const replayBlob = new Blob([compressed], {
15558
- type: 'application/gzip',
15559
- });
15560
- files.push({
15561
- type: 'replay',
15562
- url: replayUrl.uploadUrl,
15563
- key: replayUrl.storageKey,
15564
- blob: replayBlob,
15565
- });
15566
- }
15567
- return files;
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
- async uploadToStorage(files) {
15575
- const uploadPromises = files.map(async (file) => {
15576
- const controller = new AbortController();
15577
- const timeoutId = setTimeout(() => controller.abort(), FileUploadHandler.UPLOAD_TIMEOUT_MS);
15578
- try {
15579
- // Do NOT add Content-Type header - it's already included in the presigned URL signature
15580
- // Adding it here will cause a signature mismatch and 403 Forbidden error
15581
- const response = await fetch(file.url, {
15582
- method: 'PUT',
15583
- body: file.blob,
15584
- signal: controller.signal,
15585
- });
15586
- clearTimeout(timeoutId);
15587
- return { success: response.ok, type: file.type };
15588
- }
15589
- catch (error) {
15590
- clearTimeout(timeoutId);
15591
- logger$3.error(`Upload failed for ${file.type}:`, error);
15592
- return { success: false, type: file.type };
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
- async confirmUploads(files, bugId) {
15607
- const confirmPromises = files.map(async (file) => {
15608
- try {
15609
- const response = await fetch(`${this.apiEndpoint}/api/v1/reports/${bugId}/confirm-upload`, {
15610
- method: 'POST',
15611
- headers: {
15612
- 'Content-Type': 'application/json',
15613
- 'X-API-Key': this.apiKey,
15614
- },
15615
- body: JSON.stringify({
15616
- fileType: file.type,
15617
- }),
15618
- });
15619
- return { success: response.ok, type: file.type };
15620
- }
15621
- catch (error) {
15622
- logger$3.error(`Confirmation failed for ${file.type}:`, error);
15623
- return { success: false, type: file.type };
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
- async dataUrlToBlob(dataUrl) {
15648
- if (!dataUrl || !dataUrl.startsWith('data:')) {
15649
- throw new Error('Invalid data URL');
15650
- }
15651
- const response = await fetch(dataUrl);
15652
- if (!response || !response.blob) {
15653
- throw new Error('Failed to convert data URL to Blob');
15654
- }
15655
- return await response.blob();
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
- queue.push(this.createQueuedRequest(endpoint, serializedBody, headers));
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
- async process(retryableStatusCodes) {
15944
- const queue = this.getQueue();
15945
- if (queue.length === 0) {
15946
- return;
15947
- }
15948
- this.logger.log(`Processing offline queue (${queue.length} requests)`);
15949
- const successfulIds = [];
15950
- const failedRequests = [];
15951
- for (const request of queue) {
15952
- // Check if request has exceeded max retry attempts
15953
- if (request.attempts >= MAX_RETRY_ATTEMPTS) {
15954
- this.logger.warn(`Max retry attempts (${MAX_RETRY_ATTEMPTS}) reached for request (id: ${request.id}), removing`);
15955
- continue;
15956
- }
15957
- // Check if request has expired
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
- try {
15965
- // Attempt to send
15966
- const response = await fetch(request.endpoint, {
15967
- method: 'POST',
15968
- headers: request.headers,
15969
- body: request.body,
15970
- });
15971
- if (response.ok) {
15972
- this.logger.log(`Successfully sent queued request (id: ${request.id})`);
15973
- successfulIds.push(request.id);
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
- else if (retryableStatusCodes.includes(response.status)) {
15976
- // Keep in queue for next attempt
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 status ${response.status}, will retry later (id: ${request.id})`);
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
- catch (error) {
15987
- // Network error, keep in queue
15988
- request.attempts++;
15989
- failedRequests.push(request);
15990
- this.logger.warn(`Queued request failed with network error, will retry later (id: ${request.id}):`, error);
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
- async executeWithRetry(operation, shouldRetryStatus) {
16210
- let lastError = null;
16211
- for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
16212
- try {
16213
- const response = await operation();
16214
- // Check if we should retry based on status code
16215
- if (shouldRetryStatus(response.status) &&
16216
- attempt < this.config.maxRetries) {
16217
- const delay = this.calculateDelay(attempt, response);
16218
- this.logger.warn(`Request failed with status ${response.status}, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries})`);
16219
- await sleep(delay);
16220
- continue;
16221
- }
16222
- // Success or non-retryable status
16223
- return response;
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
- // Retry on network errors
16232
- if (attempt < this.config.maxRetries) {
16233
- const delay = this.calculateDelay(attempt);
16234
- this.logger.warn(`Network error, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries}):`, error);
16235
- await sleep(delay);
16236
- continue;
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
- // All retries exhausted
16241
- throw lastError || new Error('Request failed after all retry attempts');
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
- if ((_b = (_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.has) === null || _b === void 0 ? void 0 : _b.call(_a, 'Retry-After')) {
16250
- const retryAfter = response.headers.get('Retry-After');
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
- async function processQueueInBackground(offlineConfig, retryConfig, logger) {
16272
- if (!offlineConfig.enabled) {
16273
- return;
16274
- }
16275
- const queue = new OfflineQueue(offlineConfig, logger);
16276
- queue.process(retryConfig.retryOn).catch((error) => {
16277
- logger.warn('Failed to process offline queue:', error);
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
- async function handleOfflineFailure(error, endpoint, body, contentHeaders, auth, offlineConfig, logger) {
16284
- if (!offlineConfig.enabled || !isNetworkError(error)) {
16285
- return;
16286
- }
16287
- logger.warn('Network error detected, queueing request for offline retry');
16288
- const queue = new OfflineQueue(offlineConfig, logger);
16289
- const authHeaders = generateAuthHeaders(auth);
16290
- await queue.enqueue(endpoint, body, Object.assign(Object.assign({}, contentHeaders), authHeaders));
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
- async function submitWithAuth(endpoint, body, contentHeaders = {}, options) {
16313
- const logger = options.logger || getLogger();
16314
- const retryConfig = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), options.retry);
16315
- const offlineConfig = Object.assign(Object.assign({}, DEFAULT_OFFLINE_CONFIG), options.offline);
16316
- // Process offline queue on each request (run in background without awaiting)
16317
- processQueueInBackground(offlineConfig, retryConfig, logger);
16318
- try {
16319
- // Send with retry logic
16320
- const response = await sendWithRetry(endpoint, body, contentHeaders, options.auth, retryConfig, logger);
16321
- return response;
16322
- }
16323
- catch (error) {
16324
- // Queue for offline retry if enabled
16325
- await handleOfflineFailure(error, endpoint, body, contentHeaders, options.auth, offlineConfig, logger);
16326
- throw error;
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
- async function makeRequest(endpoint, body, contentHeaders, auth) {
16336
- const authHeaders = generateAuthHeaders(auth);
16337
- const headers = Object.assign(Object.assign({}, contentHeaders), authHeaders);
16338
- return fetch(endpoint, {
16339
- method: 'POST',
16340
- headers,
16341
- body,
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
- async function sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger) {
16348
- const retryHandler = new RetryHandler(retryConfig, logger);
16349
- return retryHandler.executeWithRetry(async () => makeRequest(endpoint, body, contentHeaders, auth), (status) => retryConfig.retryOn.includes(status));
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
- async submit(payload) {
16484
- const dedupContext = this.validateAndExtractErrors(payload);
16485
- try {
16486
- logger$1.debug(`Submitting bug report to ${this.config.endpoint}`);
16487
- const bugReportData = await this.createBugReport(payload);
16488
- await this.handleFileUploads(bugReportData, payload, dedupContext);
16489
- }
16490
- finally {
16491
- // Mark this specific report submission as complete
16492
- this.deduplicator.markComplete(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
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
- async createBugReport(payload) {
16523
- const { report } = payload, metadata = __rest(payload, ["report"]);
16524
- const fileAnalysis = analyzeReportFiles(report);
16525
- logger$1.debug('File upload detection', fileAnalysis);
16526
- const createPayload = Object.assign(Object.assign({}, metadata), { report: {
16527
- console: report.console,
16528
- network: report.network,
16529
- metadata: report.metadata,
16530
- }, hasScreenshot: fileAnalysis.hasScreenshot, hasReplay: fileAnalysis.hasReplay });
16531
- const response = await submitWithAuth(this.config.endpoint, JSON.stringify(createPayload), { 'Content-Type': 'application/json' }, {
16532
- auth: this.config.auth,
16533
- retry: this.config.retry,
16534
- offline: this.config.offline,
16535
- });
16536
- if (!response.ok) {
16537
- const errorText = await response.text().catch(() => 'Unknown error');
16538
- throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
16539
- }
16540
- const result = await response.json();
16541
- if (!isBugReportResponse(result)) {
16542
- throw new Error('Invalid server response format');
16543
- }
16544
- if (!result.success) {
16545
- throw new Error('Bug report creation failed on server');
16546
- }
16547
- // TypeScript now knows result.success is true, so result.data exists (validated by type guard)
16548
- const bugData = result.data;
16549
- logger$1.debug('Bug report creation response', {
16550
- success: result.success,
16551
- bugId: bugData.id,
16552
- hasPresignedUrls: !!bugData.presignedUrls,
16553
- presignedUrlKeys: bugData.presignedUrls
16554
- ? Object.keys(bugData.presignedUrls)
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
- async handleFileUploads(bugReportData, payload, dedupContext) {
16564
- // bugReportData.id is guaranteed to exist by type guard validation in createBugReport
16565
- const bugId = bugReportData.id;
16566
- const { report } = payload;
16567
- const fileAnalysis = analyzeReportFiles(report);
16568
- if (!fileAnalysis.hasScreenshot && !fileAnalysis.hasReplay) {
16569
- logger$1.debug('No files to upload, bug report created successfully', {
16570
- bugId,
16571
- });
16572
- this.deduplicator.recordSubmission(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
16573
- return;
16574
- }
16575
- if (!bugReportData.presignedUrls) {
16576
- logger$1.error('Presigned URLs not returned despite requesting file uploads', {
16577
- bugId,
16578
- hasScreenshot: fileAnalysis.hasScreenshot,
16579
- hasReplay: fileAnalysis.hasReplay,
16580
- });
16581
- throw new Error('Server did not provide presigned URLs for file uploads. Check backend configuration.');
16582
- }
16583
- const apiEndpoint = getApiBaseUrl(this.config.endpoint);
16584
- const uploadHandler = new FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
16585
- try {
16586
- await uploadHandler.uploadFiles(bugId, report, bugReportData.presignedUrls);
16587
- logger$1.debug('File uploads completed successfully', { bugId });
16588
- this.deduplicator.recordSubmission(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
16589
- }
16590
- catch (error) {
16591
- logger$1.error('File upload failed', {
16592
- bugId,
16593
- error: formatSubmissionError('Upload', error),
16594
- });
16595
- throw new Error(formatSubmissionError(`Bug report created (ID: ${bugId}) but file upload failed`, error));
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
- async uploadScreenshot(file, onProgress) {
16625
- return this.uploadFile(file, 'screenshot', 'screenshot.png', onProgress);
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
- async uploadReplay(compressedData, onProgress) {
16634
- return this.uploadFile(compressedData, 'replay', 'replay.gz', onProgress);
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
- async uploadAttachment(file, onProgress) {
16643
- return this.uploadFile(file, 'attachment', file.name, onProgress);
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
- async uploadFile(file, fileType, filename, onProgress) {
16652
- try {
16653
- // Step 1: Get presigned upload URL
16654
- const presignedUrlResponse = await this.requestPresignedUrl(fileType, filename);
16655
- if (!presignedUrlResponse.success) {
16656
- return {
16657
- success: false,
16658
- error: presignedUrlResponse.error || 'Failed to get presigned URL',
16659
- };
16660
- }
16661
- const { uploadUrl, storageKey } = presignedUrlResponse.data;
16662
- // Step 2: Upload file to storage using presigned URL
16663
- const uploadSuccess = await this.uploadToStorage(uploadUrl, file, onProgress);
16664
- if (!uploadSuccess) {
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: false,
16667
- error: 'Failed to upload file to storage',
16938
+ success: true,
16939
+ storageKey,
16668
16940
  };
16669
16941
  }
16670
- // Step 3: Confirm upload with API
16671
- const confirmSuccess = await this.confirmUpload(fileType);
16672
- if (!confirmSuccess) {
16942
+ catch (error) {
16673
16943
  return {
16674
16944
  success: false,
16675
- error: 'Failed to confirm upload',
16945
+ error: error instanceof Error ? error.message : 'Unknown error',
16676
16946
  };
16677
16947
  }
16678
- return {
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
- async requestPresignedUrl(fileType, filename) {
16694
- try {
16695
- const response = await fetch(`${this.config.apiEndpoint}/api/v1/uploads/presigned-url`, {
16696
- method: 'POST',
16697
- headers: {
16698
- 'Content-Type': 'application/json',
16699
- 'x-api-key': this.config.apiKey,
16700
- },
16701
- body: JSON.stringify({
16702
- projectId: this.config.projectId,
16703
- bugId: this.config.bugId,
16704
- fileType,
16705
- filename,
16706
- }),
16707
- });
16708
- if (!response.ok) {
16709
- const errorText = await response.text();
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: `HTTP ${response.status}: ${errorText}`,
16986
+ error: error instanceof Error ? error.message : 'Network error',
16713
16987
  };
16714
16988
  }
16715
- const result = await response.json();
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
- async uploadToStorage(uploadUrl, file, onProgress) {
16734
- // Convert File/Blob to ArrayBuffer to prevent browser from auto-setting Content-Type header
16735
- // This is critical for CORS compatibility with B2/S3 presigned URLs
16736
- const arrayBuffer = await this.fileToArrayBuffer(file);
16737
- return new Promise((resolve) => {
16738
- const xhr = new XMLHttpRequest();
16739
- // Track upload progress
16740
- if (onProgress) {
16741
- xhr.upload.addEventListener('progress', (event) => {
16742
- if (event.lengthComputable) {
16743
- onProgress({
16744
- loaded: event.loaded,
16745
- total: event.total,
16746
- percentage: Math.round((event.loaded / event.total) * 100),
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
- // Handle completion
16752
- xhr.addEventListener('load', () => {
16753
- resolve(xhr.status >= 200 && xhr.status < 300);
16754
- });
16755
- // Handle errors
16756
- xhr.addEventListener('error', () => {
16757
- resolve(false);
16758
- });
16759
- xhr.addEventListener('abort', () => {
16760
- resolve(false);
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
- async confirmUpload(fileType) {
16787
- try {
16788
- const response = await fetch(`${this.config.apiEndpoint}/api/v1/reports/${this.config.bugId}/confirm-upload`, {
16789
- method: 'POST',
16790
- headers: {
16791
- 'Content-Type': 'application/json',
16792
- 'x-api-key': this.config.apiKey,
16793
- },
16794
- body: JSON.stringify({
16795
- fileType,
16796
- }),
16797
- });
16798
- return response.ok;
16799
- }
16800
- catch (_a) {
16801
- return false;
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
- async function compressReplayEvents(events) {
16817
- // Convert events to JSON string
16818
- const jsonString = JSON.stringify(events);
16819
- const textEncoder = new TextEncoder();
16820
- const data = textEncoder.encode(jsonString);
16821
- // Check if CompressionStream is supported (Chrome 80+, Firefox 113+, Safari 16.4+)
16822
- if (typeof CompressionStream === 'undefined') {
16823
- console.warn('CompressionStream not supported, uploading uncompressed replay data');
16824
- return new Blob([data], { type: 'application/json' });
16825
- }
16826
- try {
16827
- // Use modern streaming API: Blob → ReadableStream → CompressionStream → Response → Blob
16828
- const blob = new Blob([data]);
16829
- const compressedStream = blob
16830
- .stream()
16831
- .pipeThrough(new CompressionStream('gzip'));
16832
- return await new Response(compressedStream, {
16833
- headers: { 'Content-Type': 'application/gzip' },
16834
- }).blob();
16835
- }
16836
- catch (error) {
16837
- console.error('Compression failed, uploading uncompressed:', error);
16838
- return new Blob([data], { type: 'application/json' });
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
- async function canvasToBlob(canvas, quality = 0.9) {
16848
- return new Promise((resolve, reject) => {
16849
- canvas.toBlob((blob) => {
16850
- if (blob) {
16851
- resolve(blob);
16852
- }
16853
- else {
16854
- reject(new Error('Failed to convert canvas to Blob'));
16855
- }
16856
- }, 'image/png', quality);
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
- async function fetchReplaySettings(endpoint, apiKey) {
16914
- var _a, _b, _c, _d, _e, _f, _g, _h;
16915
- const defaults = {
16916
- duration: DEFAULT_REPLAY_DURATION_SECONDS,
16917
- inline_stylesheets: true,
16918
- inline_images: false,
16919
- collect_fonts: true,
16920
- record_canvas: false,
16921
- record_cross_origin_iframes: false,
16922
- sampling_mousemove: DEFAULT_MOUSEMOVE_SAMPLING,
16923
- sampling_scroll: DEFAULT_SCROLL_SAMPLING,
16924
- };
16925
- try {
16926
- const apiBaseUrl = getApiBaseUrl(endpoint);
16927
- const headers = {};
16928
- if (apiKey) {
16929
- headers['x-api-key'] = apiKey;
16930
- }
16931
- const response = await fetch(`${apiBaseUrl}/api/v1/settings/replay`, {
16932
- headers,
16933
- });
16934
- if (!response.ok) {
16935
- logger.warn(`Failed to fetch replay settings: ${response.status}. Using defaults.`);
16936
- return defaults;
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
- const result = await response.json();
16939
- if (!result.success || !result.data) {
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
- return {
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(async () => {
16986
- await this.handleBugReport();
16987
- });
16988
- }
16989
- }
16990
- static async init(config) {
16991
- // If instance exists, warn about singleton behavior
16992
- if (BugSpotter.instance) {
16993
- logger.warn('BugSpotter.init() called multiple times. Returning existing instance. ' +
16994
- 'Call destroy() first to reinitialize with new config.');
16995
- return BugSpotter.instance;
16996
- }
16997
- // If initialization is already in progress, wait for it
16998
- if (BugSpotter.initPromise) {
16999
- logger.warn('BugSpotter.init() called while initialization in progress. Waiting...');
17000
- return BugSpotter.initPromise;
17001
- }
17002
- // Start initialization and cache the promise
17003
- BugSpotter.initPromise = BugSpotter.createInstance(config);
17004
- try {
17005
- BugSpotter.instance = await BugSpotter.initPromise;
17006
- return BugSpotter.instance;
17007
- }
17008
- finally {
17009
- // Clear the promise once initialization completes (success or failure)
17010
- BugSpotter.initPromise = undefined;
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 async createInstance(config) {
17018
- var _a, _b, _c;
17019
- // Fetch replay quality settings from backend if replay is enabled
17020
- let backendSettings = null;
17021
- const replayEnabled = (_b = (_a = config.replay) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
17022
- if (replayEnabled && config.endpoint) {
17023
- // Validate auth is configured before attempting fetch
17024
- if (!((_c = config.auth) === null || _c === void 0 ? void 0 : _c.apiKey)) {
17025
- logger.warn('Endpoint provided but no API key configured. Skipping backend settings fetch.');
17026
- }
17027
- else {
17028
- backendSettings = await fetchReplaySettings(config.endpoint, config.auth.apiKey);
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
- // Merge backend settings with user config (user config takes precedence)
17032
- const mergedConfig = Object.assign(Object.assign({}, config), { replay: mergeReplayConfig(config.replay, backendSettings) });
17033
- return new BugSpotter(mergedConfig);
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
- async capture() {
17044
- return await this.captureManager.captureAll();
17045
- }
17046
- async handleBugReport() {
17047
- const report = await this.capture();
17048
- const modal = new BugReportModal({
17049
- onSubmit: async (data) => {
17050
- logger.log('Submitting bug:', Object.assign(Object.assign({}, data), { report }));
17051
- // Send to endpoint if configured
17052
- if (this.config.endpoint) {
17053
- try {
17054
- await this.submit(Object.assign(Object.assign({}, data), { report }));
17055
- logger.log('Bug report submitted successfully');
17056
- }
17057
- catch (error) {
17058
- logger.error('Failed to submit bug report:', error);
17059
- // Re-throw to allow UI to handle errors if needed
17060
- throw error;
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
- onProgress: (message) => {
17065
- logger.debug('Upload progress:', message);
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
- async submit(payload) {
17076
- await this.bugReporter.submit(payload);
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