@bugspotter/sdk 0.3.0 → 1.0.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.
Files changed (41) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.prettierrc +11 -0
  3. package/CHANGELOG.md +91 -144
  4. package/CONTRIBUTING.md +200 -0
  5. package/README.md +18 -16
  6. package/SECURITY.md +65 -0
  7. package/dist/bugspotter.min.js +2 -1
  8. package/dist/bugspotter.min.js.map +1 -0
  9. package/dist/capture/console.js +14 -3
  10. package/dist/capture/screenshot.js +3 -2
  11. package/dist/core/buffer.js +2 -1
  12. package/dist/core/bug-reporter.js +16 -5
  13. package/dist/core/circular-buffer.js +4 -1
  14. package/dist/core/compress.js +2 -1
  15. package/dist/core/file-upload-handler.js +5 -2
  16. package/dist/core/offline-queue.d.ts +13 -0
  17. package/dist/core/offline-queue.js +54 -6
  18. package/dist/core/transport.js +24 -10
  19. package/dist/core/upload-helpers.js +3 -1
  20. package/dist/index.d.ts +6 -5
  21. package/dist/index.esm.js +17379 -149
  22. package/dist/index.esm.js.map +1 -0
  23. package/dist/index.js +5 -1
  24. package/dist/utils/config-validator.js +6 -0
  25. package/dist/utils/sanitize-patterns.js +15 -3
  26. package/dist/utils/url-helpers.d.ts +15 -0
  27. package/dist/utils/url-helpers.js +37 -1
  28. package/dist/version.d.ts +1 -1
  29. package/dist/version.js +1 -1
  30. package/dist/widget/button.d.ts +10 -0
  31. package/dist/widget/button.js +200 -3
  32. package/dist/widget/components/form-validator.js +2 -1
  33. package/dist/widget/components/style-manager.js +2 -1
  34. package/dist/widget/components/template-manager.js +2 -1
  35. package/dist/widget/modal.js +11 -4
  36. package/docs/CDN.md +5 -5
  37. package/eslint.config.js +99 -0
  38. package/package.json +39 -15
  39. package/release_notes.md +19 -0
  40. package/rollup.config.js +25 -0
  41. package/tsconfig.cjs.json +1 -1
package/dist/index.js CHANGED
@@ -62,7 +62,9 @@ async function fetchReplaySettings(endpoint, apiKey) {
62
62
  if (apiKey) {
63
63
  headers['x-api-key'] = apiKey;
64
64
  }
65
- const response = await fetch(`${apiBaseUrl}/api/v1/settings/replay`, { headers });
65
+ const response = await fetch(`${apiBaseUrl}/api/v1/settings/replay`, {
66
+ headers,
67
+ });
66
68
  if (!response.ok) {
67
69
  logger.warn(`Failed to fetch replay settings: ${response.status}. Using defaults.`);
68
70
  return defaults;
@@ -311,3 +313,5 @@ function sanitize(text) {
311
313
  });
312
314
  return sanitizer.sanitize(text);
313
315
  }
316
+ // Default export for convenience
317
+ exports.default = BugSpotter;
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.validateAuthConfig = validateAuthConfig;
8
8
  exports.validateDeduplicationConfig = validateDeduplicationConfig;
9
+ const url_helpers_1 = require("./url-helpers");
9
10
  /**
10
11
  * Validate authentication configuration
11
12
  * @throws Error if configuration is invalid
@@ -14,6 +15,11 @@ function validateAuthConfig(context) {
14
15
  if (!context.endpoint) {
15
16
  throw new Error('No endpoint configured for bug report submission');
16
17
  }
18
+ // SECURITY: Ensure endpoint uses HTTPS
19
+ // This prevents credentials and sensitive data from being sent over plain HTTP
20
+ if (!(0, url_helpers_1.isSecureEndpoint)(context.endpoint)) {
21
+ throw new url_helpers_1.InsecureEndpointError(context.endpoint);
22
+ }
17
23
  if (!context.auth) {
18
24
  throw new Error('API key authentication is required');
19
25
  }
@@ -20,7 +20,11 @@ exports.DEFAULT_PATTERNS = {
20
20
  name: 'email',
21
21
  regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
22
22
  description: 'Email addresses',
23
- examples: ['user@example.com', 'john.doe+tag@company.co.uk', 'test_user@sub.domain.com'],
23
+ examples: [
24
+ 'user@example.com',
25
+ 'john.doe+tag@company.co.uk',
26
+ 'test_user@sub.domain.com',
27
+ ],
24
28
  priority: 1, // Highest priority - most specific
25
29
  },
26
30
  creditcard: {
@@ -53,7 +57,11 @@ exports.DEFAULT_PATTERNS = {
53
57
  name: 'ip',
54
58
  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,
55
59
  description: 'IPv4 and IPv6 addresses',
56
- examples: ['192.168.1.100', '127.0.0.1', '2001:0db8:85a3:0000:0000:8a2e:0370:7334'],
60
+ examples: [
61
+ '192.168.1.100',
62
+ '127.0.0.1',
63
+ '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
64
+ ],
57
65
  priority: 5,
58
66
  },
59
67
  phone: {
@@ -96,7 +104,11 @@ exports.DEFAULT_PATTERNS = {
96
104
  name: 'password',
97
105
  regex: /(?:password|passwd|pwd)[\s:=]+[^\s]{6,}|(?:password|passwd|pwd)["']?\s*[:=]\s*["']?[^\s"']{6,}/gi,
98
106
  description: 'Password fields in text (password=..., pwd:...)',
99
- examples: ['password: MySecret123!', 'passwd=SecurePass456', 'pwd: "MyP@ssw0rd"'],
107
+ examples: [
108
+ 'password: MySecret123!',
109
+ 'passwd=SecurePass456',
110
+ 'pwd: "MyP@ssw0rd"',
111
+ ],
100
112
  priority: 9,
101
113
  },
102
114
  };
@@ -26,3 +26,18 @@ export declare function stripEndpointSuffix(path: string): string;
26
26
  * @throws InvalidEndpointError if endpoint is not a valid absolute URL
27
27
  */
28
28
  export declare function getApiBaseUrl(endpoint: string): string;
29
+ /**
30
+ * specific error for insecure endpoints
31
+ */
32
+ export declare class InsecureEndpointError extends Error {
33
+ readonly endpoint: string;
34
+ constructor(endpoint: string);
35
+ }
36
+ /**
37
+ * Checks if the endpoint uses the secure HTTPS protocol.
38
+ * Uses the URL API for robust parsing.
39
+ *
40
+ * @param endpoint The endpoint URL to check
41
+ * @returns True if the endpoint uses HTTPS
42
+ */
43
+ export declare function isSecureEndpoint(endpoint: string): boolean;
@@ -4,9 +4,10 @@
4
4
  * Extract base API URL from endpoint configuration
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.InvalidEndpointError = void 0;
7
+ exports.InsecureEndpointError = exports.InvalidEndpointError = void 0;
8
8
  exports.stripEndpointSuffix = stripEndpointSuffix;
9
9
  exports.getApiBaseUrl = getApiBaseUrl;
10
+ exports.isSecureEndpoint = isSecureEndpoint;
10
11
  const logger_1 = require("./logger");
11
12
  const logger = (0, logger_1.getLogger)();
12
13
  /**
@@ -62,3 +63,38 @@ function getApiBaseUrl(endpoint) {
62
63
  throw new InvalidEndpointError(endpoint, 'Must be a valid absolute URL (e.g., https://api.example.com/api/v1/reports)');
63
64
  }
64
65
  }
66
+ /**
67
+ * specific error for insecure endpoints
68
+ */
69
+ class InsecureEndpointError extends Error {
70
+ constructor(endpoint) {
71
+ super(`Secure HTTPS connection required. Attempted to connect to insecure endpoint: "${endpoint}"`);
72
+ this.endpoint = endpoint;
73
+ this.name = 'InsecureEndpointError';
74
+ }
75
+ }
76
+ exports.InsecureEndpointError = InsecureEndpointError;
77
+ /**
78
+ * Checks if the endpoint uses the secure HTTPS protocol.
79
+ * Uses the URL API for robust parsing.
80
+ *
81
+ * @param endpoint The endpoint URL to check
82
+ * @returns True if the endpoint uses HTTPS
83
+ */
84
+ function isSecureEndpoint(endpoint) {
85
+ if (!endpoint)
86
+ return false;
87
+ try {
88
+ const url = new URL(endpoint.trim());
89
+ // STRICT SECURITY:
90
+ // 1. Production must use HTTPS
91
+ // 2. Development allowed on localhost/127.0.0.1 via HTTP
92
+ return (url.protocol === 'https:' ||
93
+ (url.protocol === 'http:' &&
94
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1')));
95
+ }
96
+ catch (_a) {
97
+ // If it's not a valid URL, it's definitely not a secure endpoint
98
+ return false;
99
+ }
100
+ }
package/dist/version.d.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * This file is automatically generated during the build process.
6
6
  * To update the version, modify package.json
7
7
  */
8
- export declare const VERSION = "0.3.0";
8
+ export declare const VERSION = "1.0.0";
package/dist/version.js CHANGED
@@ -8,4 +8,4 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.VERSION = void 0;
11
- exports.VERSION = '0.3.0';
11
+ exports.VERSION = '1.0.0';
@@ -21,6 +21,16 @@ export declare class FloatingButton {
21
21
  private eventHandlers;
22
22
  constructor(options?: FloatingButtonOptions);
23
23
  private createButton;
24
+ /**
25
+ * Safely inject HTML content by parsing and validating SVG elements
26
+ * Prevents XSS attacks by only allowing safe SVG elements and attributes
27
+ */
28
+ private setSafeHTMLContent;
29
+ /**
30
+ * Recursively sanitize SVG elements by removing dangerous tags and attributes
31
+ * Uses whitelists to ensure only safe SVG content is preserved
32
+ */
33
+ private sanitizeSVGElement;
24
34
  private getButtonStyles;
25
35
  private getPositionStyles;
26
36
  private handleMouseEnter;
@@ -37,6 +37,104 @@ const BUTTON_STYLES = {
37
37
  active: 'scale(0.95)',
38
38
  },
39
39
  };
40
+ // SVG sanitization whitelists (module-level for performance)
41
+ const SAFE_SVG_TAGS = new Set([
42
+ 'svg',
43
+ 'g',
44
+ 'path',
45
+ 'circle',
46
+ 'rect',
47
+ 'line',
48
+ 'polyline',
49
+ 'polygon',
50
+ 'ellipse',
51
+ 'text',
52
+ 'tspan',
53
+ // SECURITY: 'use' deliberately excluded - requires href/xlink:href attributes which pose XSS risks
54
+ // and are not in the attribute whitelist, making <use> non-functional anyway
55
+ 'symbol',
56
+ 'defs',
57
+ 'marker',
58
+ 'lineargradient', // lowercase to match tagName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
59
+ 'radialgradient', // lowercase to match tagName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
60
+ 'stop',
61
+ 'clippath', // lowercase to match tagName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
62
+ 'mask',
63
+ // SECURITY: 'image' deliberately excluded - requires href/xlink:href attributes which pose XSS risks
64
+ // and are not in the attribute whitelist, making <image> non-functional anyway
65
+ // SECURITY: foreignObject deliberately excluded - allows embedding arbitrary HTML/XML
66
+ // and can bypass SVG sanitization (e.g., <foreignObject><body><script>...</script></body></foreignObject>)
67
+ ]);
68
+ const SAFE_SVG_ATTRIBUTES = new Set([
69
+ 'id',
70
+ 'class',
71
+ // SECURITY: 'style' deliberately excluded - can enable CSS-based attacks:
72
+ // - expression() in older browsers
73
+ // - url() with javascript:/data: URIs
74
+ // - @import with malicious stylesheets
75
+ // - CSS data exfiltration
76
+ // Use specific styling attributes (fill, stroke, opacity, etc.) instead
77
+ 'd',
78
+ 'cx',
79
+ 'cy',
80
+ 'r',
81
+ 'rx',
82
+ 'ry',
83
+ 'x',
84
+ 'y',
85
+ 'x1',
86
+ 'y1',
87
+ 'x2',
88
+ 'y2',
89
+ 'width',
90
+ 'height',
91
+ 'viewbox', // lowercase to match attrName.toLowerCase() in sanitizeSVGElement (parser preserves camelCase)
92
+ 'xmlns',
93
+ 'fill',
94
+ 'stroke',
95
+ 'stroke-width',
96
+ 'stroke-linecap',
97
+ 'stroke-linejoin',
98
+ 'opacity',
99
+ 'fill-opacity',
100
+ 'stroke-opacity',
101
+ 'transform',
102
+ 'points',
103
+ 'text-anchor',
104
+ 'font-size',
105
+ 'font-family',
106
+ 'font-weight',
107
+ 'offset',
108
+ 'stop-color',
109
+ 'stop-opacity',
110
+ 'clip-path',
111
+ 'mask', // Used to reference mask definitions: mask="url(#maskId)"
112
+ ]);
113
+ /**
114
+ * Check if an attribute value contains dangerous patterns
115
+ * Uses simple string matching instead of regex for better performance and clarity
116
+ */
117
+ const isDangerousAttributeValue = (value) => {
118
+ const lowerValue = value.toLowerCase();
119
+ // Dangerous protocol checks
120
+ if (lowerValue.includes('javascript:'))
121
+ return true;
122
+ if (lowerValue.includes('vbscript:'))
123
+ return true;
124
+ // SECURITY: Block ALL data: URIs by default
125
+ // data:text/html, data:application/javascript, data:image/svg+xml can all execute scripts
126
+ // Even data:text/javascript or data URIs with embedded scripts are dangerous
127
+ if (lowerValue.includes('data:'))
128
+ return true;
129
+ // CSS-based attack patterns
130
+ if (lowerValue.includes('expression('))
131
+ return true; // IE CSS expressions
132
+ if (lowerValue.includes('@import'))
133
+ return true; // CSS imports
134
+ if (lowerValue.includes('-moz-binding'))
135
+ return true; // Firefox XBL binding
136
+ return false;
137
+ };
40
138
  class FloatingButton {
41
139
  constructor(options = {}) {
42
140
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -80,10 +178,12 @@ class FloatingButton {
80
178
  const btn = document.createElement('button');
81
179
  // Set button content (SVG or text)
82
180
  if (this.options.customSvg) {
83
- btn.innerHTML = this.options.customSvg;
181
+ // Safely inject custom SVG by parsing and validating it
182
+ this.setSafeHTMLContent(btn, this.options.customSvg);
84
183
  }
85
184
  else if (this.options.icon === 'svg') {
86
- btn.innerHTML = DEFAULT_SVG_ICON;
185
+ // Safely inject default SVG
186
+ this.setSafeHTMLContent(btn, DEFAULT_SVG_ICON);
87
187
  }
88
188
  else {
89
189
  btn.textContent = this.options.icon;
@@ -95,6 +195,103 @@ class FloatingButton {
95
195
  this.addHoverEffects(btn);
96
196
  return btn;
97
197
  }
198
+ /**
199
+ * Safely inject HTML content by parsing and validating SVG elements
200
+ * Prevents XSS attacks by only allowing safe SVG elements and attributes
201
+ */
202
+ setSafeHTMLContent(element, htmlContent) {
203
+ try {
204
+ if (typeof window === 'undefined' ||
205
+ typeof window.DOMParser === 'undefined') {
206
+ element.textContent = htmlContent;
207
+ return;
208
+ }
209
+ // SECURITY: Use DOMParser with image/svg+xml MIME type for strict SVG parsing
210
+ // This prevents HTML-specific parsing quirks from being exploited
211
+ const parser = new window.DOMParser();
212
+ const doc = parser.parseFromString(htmlContent, 'image/svg+xml');
213
+ // Check for parse errors
214
+ const parserError = doc.querySelector('parsererror');
215
+ if (parserError) {
216
+ element.textContent = htmlContent;
217
+ return;
218
+ }
219
+ if (doc.documentElement &&
220
+ doc.documentElement.nodeType === Node.ELEMENT_NODE) {
221
+ const rootElement = doc.documentElement;
222
+ // SECURITY: Root element MUST be SVG - prevents wrapper element injection
223
+ // Reject structures like <div><svg>...</svg></div>
224
+ if (rootElement.tagName.toLowerCase() === 'svg') {
225
+ // SECURITY: Only proceed if there's exactly one root element
226
+ // This prevents attacks like: <svg></svg><script>alert('XSS')</script>
227
+ if (doc.children.length === 1) {
228
+ // Remove potentially dangerous attributes and event handlers
229
+ this.sanitizeSVGElement(rootElement);
230
+ // Clear the target element and append only the validated SVG element
231
+ element.innerHTML = '';
232
+ element.appendChild(rootElement);
233
+ return;
234
+ }
235
+ }
236
+ }
237
+ // If not valid SVG, fall back to text content to prevent XSS
238
+ element.textContent = htmlContent;
239
+ }
240
+ catch (error) {
241
+ // On any error, use text content for safety
242
+ // eslint-disable-next-line no-console
243
+ console.warn('[BugSpotter] Failed to inject custom SVG content:', error);
244
+ element.textContent = htmlContent;
245
+ }
246
+ }
247
+ /**
248
+ * Recursively sanitize SVG elements by removing dangerous tags and attributes
249
+ * Uses whitelists to ensure only safe SVG content is preserved
250
+ */
251
+ sanitizeSVGElement(element) {
252
+ // Process all elements in the tree
253
+ const elementsToProcess = [element];
254
+ const processedElements = new WeakSet();
255
+ while (elementsToProcess.length > 0) {
256
+ const current = elementsToProcess.pop();
257
+ if (!current || processedElements.has(current))
258
+ continue;
259
+ processedElements.add(current);
260
+ // SECURITY: First, sanitize the current element's attributes (including root)
261
+ // This prevents attacks like <svg onload="alert('XSS')">
262
+ Array.from(current.attributes || []).forEach((attr) => {
263
+ const attrName = attr.name.toLowerCase();
264
+ // SECURITY: Explicitly reject all event handler attributes (on*)
265
+ // This provides defense-in-depth and prevents accidental whitelisting
266
+ if (attrName.startsWith('on')) {
267
+ current.removeAttribute(attr.name);
268
+ return;
269
+ }
270
+ // Only keep whitelisted attributes
271
+ if (!SAFE_SVG_ATTRIBUTES.has(attrName)) {
272
+ current.removeAttribute(attr.name);
273
+ return;
274
+ }
275
+ // Check attribute values for dangerous patterns
276
+ if (isDangerousAttributeValue(attr.value)) {
277
+ current.removeAttribute(attr.name);
278
+ return;
279
+ }
280
+ });
281
+ // Then, process children elements
282
+ const children = Array.from(current.children || []);
283
+ children.forEach((child) => {
284
+ const tagName = child.tagName.toLowerCase();
285
+ // SECURITY: Remove tags not in whitelist (blocks <script>, <style>, <iframe>, etc.)
286
+ if (!SAFE_SVG_TAGS.has(tagName)) {
287
+ child.remove();
288
+ return;
289
+ }
290
+ // Add to processing queue for recursive sanitization
291
+ elementsToProcess.push(child);
292
+ });
293
+ }
294
+ }
98
295
  getButtonStyles() {
99
296
  const { position, size, offset, backgroundColor, zIndex } = this.options;
100
297
  const positionStyles = this.getPositionStyles(position, offset);
@@ -159,7 +356,7 @@ class FloatingButton {
159
356
  setIcon(icon) {
160
357
  this.options.icon = icon;
161
358
  if (icon === 'svg') {
162
- this.button.innerHTML = DEFAULT_SVG_ICON;
359
+ this.setSafeHTMLContent(this.button, DEFAULT_SVG_ICON);
163
360
  }
164
361
  else {
165
362
  this.button.textContent = icon;
@@ -31,7 +31,8 @@ class FormValidator {
31
31
  }
32
32
  // Validate PII confirmation if PII detected
33
33
  if (data.piiDetected && !data.piiConfirmed) {
34
- errors.piiConfirmation = 'Please confirm you have reviewed sensitive information';
34
+ errors.piiConfirmation =
35
+ 'Please confirm you have reviewed sensitive information';
35
36
  }
36
37
  return {
37
38
  isValid: Object.keys(errors).length === 0,
@@ -51,7 +51,8 @@ class StyleManager {
51
51
  primaryColor: config.primaryColor || '#007bff',
52
52
  dangerColor: config.dangerColor || '#dc3545',
53
53
  borderRadius: config.borderRadius || '4px',
54
- fontFamily: config.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
54
+ fontFamily: config.fontFamily ||
55
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
55
56
  zIndex: config.zIndex || 999999,
56
57
  };
57
58
  }
@@ -12,7 +12,8 @@ class TemplateManager {
12
12
  this.config = {
13
13
  title: config.title || 'Report a Bug',
14
14
  titlePlaceholder: config.titlePlaceholder || 'Brief description of the issue',
15
- descriptionPlaceholder: config.descriptionPlaceholder || 'Detailed description of what happened...',
15
+ descriptionPlaceholder: config.descriptionPlaceholder ||
16
+ 'Detailed description of what happened...',
16
17
  submitButtonText: config.submitButtonText || 'Submit Bug Report',
17
18
  cancelButtonText: config.cancelButtonText || 'Cancel',
18
19
  showScreenshot: config.showScreenshot !== false,
@@ -126,7 +126,10 @@ class BugReportModal {
126
126
  });
127
127
  // Submit button click (manually trigger form submit for test compatibility)
128
128
  elements.submitButton.addEventListener('click', () => {
129
- const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
129
+ const submitEvent = new Event('submit', {
130
+ bubbles: true,
131
+ cancelable: true,
132
+ });
130
133
  elements.form.dispatchEvent(submitEvent);
131
134
  });
132
135
  // Cancel button
@@ -159,7 +162,9 @@ class BugReportModal {
159
162
  }
160
163
  validateField(fieldName) {
161
164
  const elements = this.domCache.get();
162
- const value = fieldName === 'title' ? elements.titleInput.value : elements.descriptionTextarea.value;
165
+ const value = fieldName === 'title'
166
+ ? elements.titleInput.value
167
+ : elements.descriptionTextarea.value;
163
168
  const error = this.validator.validateField(fieldName, value);
164
169
  const errorElement = fieldName === 'title' ? elements.titleError : elements.descriptionError;
165
170
  if (error) {
@@ -278,7 +283,8 @@ class BugReportModal {
278
283
  updateProgress('Preparing screenshot...');
279
284
  // Prepare screenshot with redactions
280
285
  let finalScreenshot = this.originalScreenshot;
281
- if (this.redactionCanvas && this.redactionCanvas.getRedactions().length > 0) {
286
+ if (this.redactionCanvas &&
287
+ this.redactionCanvas.getRedactions().length > 0) {
282
288
  try {
283
289
  finalScreenshot = await this.screenshotProcessor.mergeRedactions(this.originalScreenshot, this.redactionCanvas.getCanvas());
284
290
  }
@@ -302,7 +308,8 @@ class BugReportModal {
302
308
  catch (error) {
303
309
  (0, logger_1.getLogger)().error('Error submitting bug report:', error);
304
310
  // Show error message in modal instead of blocking alert
305
- elements.submitError.textContent = 'Failed to submit bug report. Please try again.';
311
+ elements.submitError.textContent =
312
+ 'Failed to submit bug report. Please try again.';
306
313
  elements.submitError.style.display = 'block';
307
314
  // Clear stale progress status for screen readers
308
315
  elements.progressStatus.textContent = '';
package/docs/CDN.md CHANGED
@@ -30,7 +30,7 @@ Add the SDK to your HTML file:
30
30
  **Example:**
31
31
 
32
32
  ```html
33
- <script src="https://cdn.bugspotter.io/sdk/bugspotter-0.1.0.min.js"></script>
33
+ <script src="https://cdn.bugspotter.io/sdk/bugspotter-1.0.0.min.js"></script>
34
34
  ```
35
35
 
36
36
  ### Development (Latest)
@@ -49,8 +49,8 @@ For enhanced security, use SRI hashes to verify file integrity:
49
49
 
50
50
  ```html
51
51
  <script
52
- src="https://cdn.bugspotter.io/sdk/bugspotter-0.1.0.min.js"
53
- integrity="sha384-..."
52
+ src="https://cdn.bugspotter.io/sdk/bugspotter-1.0.0.min.js"
53
+ integrity="sha384-WmzRwRsJDYQTHnPU0mTuz+VqnCFn70GlSiGh6lsogKahPBEB48pTzfEEB71+uA7I"
54
54
  crossorigin="anonymous"
55
55
  ></script>
56
56
  ```
@@ -58,7 +58,7 @@ For enhanced security, use SRI hashes to verify file integrity:
58
58
  To generate SRI hash for a specific version:
59
59
 
60
60
  ```bash
61
- curl https://cdn.bugspotter.io/sdk/bugspotter-0.1.0.min.js | openssl dgst -sha384 -binary | openssl base64 -A
61
+ curl https://cdn.bugspotter.io/sdk/bugspotter-1.0.0.min.js | openssl dgst -sha384 -binary | openssl base64 -A
62
62
  ```
63
63
 
64
64
  ## 📝 Complete Example
@@ -75,7 +75,7 @@ curl https://cdn.bugspotter.io/sdk/bugspotter-0.1.0.min.js | openssl dgst -sha38
75
75
  <button id="trigger-error">Trigger Test Error</button>
76
76
 
77
77
  <!-- Load BugSpotter SDK -->
78
- <script src="https://cdn.bugspotter.io/sdk/bugspotter-0.1.0.min.js"></script>
78
+ <script src="https://cdn.bugspotter.io/sdk/bugspotter-1.0.0.min.js"></script>
79
79
 
80
80
  <script>
81
81
  // Initialize BugSpotter
@@ -0,0 +1,99 @@
1
+ import js from '@eslint/js';
2
+ import tsParser from '@typescript-eslint/parser';
3
+ import tsPlugin from '@typescript-eslint/eslint-plugin';
4
+ import prettierConfig from 'eslint-config-prettier';
5
+
6
+ export default [
7
+ js.configs.recommended,
8
+ {
9
+ files: ['**/*.{ts,js}'],
10
+ languageOptions: {
11
+ parser: tsParser,
12
+ parserOptions: {
13
+ ecmaVersion: 2020,
14
+ sourceType: 'module',
15
+ },
16
+ globals: {
17
+ // Browser globals
18
+ window: 'readonly',
19
+ document: 'readonly',
20
+ console: 'readonly',
21
+ navigator: 'readonly',
22
+ localStorage: 'readonly',
23
+ fetch: 'readonly',
24
+ XMLHttpRequest: 'readonly',
25
+ Request: 'readonly',
26
+ Response: 'readonly',
27
+ URL: 'readonly',
28
+ Blob: 'readonly',
29
+ File: 'readonly',
30
+ FileReader: 'readonly',
31
+ Image: 'readonly',
32
+ setTimeout: 'readonly',
33
+ clearTimeout: 'readonly',
34
+ setInterval: 'readonly',
35
+ clearInterval: 'readonly',
36
+ crypto: 'readonly',
37
+ alert: 'readonly',
38
+ // DOM types
39
+ HTMLElement: 'readonly',
40
+ HTMLDivElement: 'readonly',
41
+ HTMLButtonElement: 'readonly',
42
+ HTMLFormElement: 'readonly',
43
+ HTMLInputElement: 'readonly',
44
+ HTMLTextAreaElement: 'readonly',
45
+ HTMLImageElement: 'readonly',
46
+ HTMLCanvasElement: 'readonly',
47
+ HTMLStyleElement: 'readonly',
48
+ Element: 'readonly',
49
+ Node: 'readonly',
50
+ Document: 'readonly',
51
+ ShadowRoot: 'readonly',
52
+ Event: 'readonly',
53
+ MouseEvent: 'readonly',
54
+ KeyboardEvent: 'readonly',
55
+ EventListener: 'readonly',
56
+ CanvasRenderingContext2D: 'readonly',
57
+ // Web APIs
58
+ TextEncoder: 'readonly',
59
+ TextDecoder: 'readonly',
60
+ AbortController: 'readonly',
61
+ CompressionStream: 'readonly',
62
+ Console: 'readonly',
63
+ BlobPart: 'readonly',
64
+ BodyInit: 'readonly',
65
+ },
66
+ },
67
+ plugins: {
68
+ '@typescript-eslint': tsPlugin,
69
+ },
70
+ rules: {
71
+ ...tsPlugin.configs.recommended.rules,
72
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
73
+ '@typescript-eslint/no-explicit-any': 'warn',
74
+ '@typescript-eslint/explicit-function-return-type': 'off',
75
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
76
+ '@typescript-eslint/no-non-null-assertion': 'warn',
77
+ 'no-console': 'warn',
78
+ 'prefer-const': 'error',
79
+ },
80
+ },
81
+ {
82
+ files: ['**/*.test.{ts,js}', '**/*.spec.{ts,js}'],
83
+ languageOptions: {
84
+ globals: {
85
+ global: 'readonly',
86
+ Headers: 'readonly',
87
+ process: 'readonly',
88
+ ReadableStream: 'readonly',
89
+ WritableStream: 'readonly',
90
+ TransformStream: 'readonly',
91
+ },
92
+ },
93
+ rules: {
94
+ 'no-console': 'off',
95
+ '@typescript-eslint/no-explicit-any': 'off',
96
+ },
97
+ },
98
+ prettierConfig,
99
+ ];