@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.
- package/.husky/pre-commit +1 -0
- package/.prettierrc +11 -0
- package/CHANGELOG.md +91 -144
- package/CONTRIBUTING.md +200 -0
- package/README.md +18 -16
- package/SECURITY.md +65 -0
- package/dist/bugspotter.min.js +2 -1
- package/dist/bugspotter.min.js.map +1 -0
- package/dist/capture/console.js +14 -3
- package/dist/capture/screenshot.js +3 -2
- package/dist/core/buffer.js +2 -1
- package/dist/core/bug-reporter.js +16 -5
- package/dist/core/circular-buffer.js +4 -1
- package/dist/core/compress.js +2 -1
- package/dist/core/file-upload-handler.js +5 -2
- package/dist/core/offline-queue.d.ts +13 -0
- package/dist/core/offline-queue.js +54 -6
- package/dist/core/transport.js +24 -10
- package/dist/core/upload-helpers.js +3 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.esm.js +17379 -149
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/utils/config-validator.js +6 -0
- package/dist/utils/sanitize-patterns.js +15 -3
- package/dist/utils/url-helpers.d.ts +15 -0
- package/dist/utils/url-helpers.js +37 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/widget/button.d.ts +10 -0
- package/dist/widget/button.js +200 -3
- package/dist/widget/components/form-validator.js +2 -1
- package/dist/widget/components/style-manager.js +2 -1
- package/dist/widget/components/template-manager.js +2 -1
- package/dist/widget/modal.js +11 -4
- package/docs/CDN.md +5 -5
- package/eslint.config.js +99 -0
- package/package.json +39 -15
- package/release_notes.md +19 -0
- package/rollup.config.js +25 -0
- 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`, {
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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
package/dist/version.js
CHANGED
package/dist/widget/button.d.ts
CHANGED
|
@@ -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;
|
package/dist/widget/button.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 ||
|
|
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 ||
|
|
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,
|
package/dist/widget/modal.js
CHANGED
|
@@ -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', {
|
|
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'
|
|
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 &&
|
|
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 =
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
78
|
+
<script src="https://cdn.bugspotter.io/sdk/bugspotter-1.0.0.min.js"></script>
|
|
79
79
|
|
|
80
80
|
<script>
|
|
81
81
|
// Initialize BugSpotter
|
package/eslint.config.js
ADDED
|
@@ -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
|
+
];
|