@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.
@@ -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;
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
package/eslint.config.js CHANGED
@@ -80,6 +80,16 @@ export default [
80
80
  },
81
81
  {
82
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
+ },
83
93
  rules: {
84
94
  'no-console': 'off',
85
95
  '@typescript-eslint/no-explicit-any': 'off',
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@bugspotter/sdk",
3
- "version": "0.3.1",
3
+ "version": "1.1.0",
4
4
  "description": "Professional bug reporting SDK with screenshots, session replay, and automatic error capture for web applications",
5
+ "packageManager": "pnpm@9.15.0",
5
6
  "main": "dist/index.js",
6
7
  "module": "dist/index.esm.js",
7
8
  "browser": "dist/bugspotter.min.js",
@@ -55,8 +56,7 @@
55
56
  "license": "MIT",
56
57
  "repository": {
57
58
  "type": "git",
58
- "url": "https://github.com/apexbridge-tech/bugspotter-sdk.git",
59
- "directory": "packages/core"
59
+ "url": "https://github.com/apexbridge-tech/bugspotter-sdk.git"
60
60
  },
61
61
  "bugs": {
62
62
  "url": "https://github.com/apexbridge-tech/bugspotter-sdk/issues"
@@ -69,7 +69,17 @@
69
69
  "access": "public",
70
70
  "registry": "https://registry.npmjs.org/"
71
71
  },
72
+ "lint-staged": {
73
+ "*.{ts,js}": [
74
+ "eslint --fix",
75
+ "prettier --write"
76
+ ],
77
+ "*.{json,md}": [
78
+ "prettier --write"
79
+ ]
80
+ },
72
81
  "dependencies": {
82
+ "@bugspotter/common": "^1.0.1",
73
83
  "@rrweb/types": "2.0.0-alpha.18",
74
84
  "html-to-image": "^1.11.13",
75
85
  "pako": "^2.1.0",
@@ -90,8 +100,9 @@
90
100
  "eslint": "^9.17.0",
91
101
  "eslint-config-prettier": "^10.1.8",
92
102
  "eslint-plugin-prettier": "^5.2.1",
93
- "happy-dom": "^19.0.2",
103
+ "happy-dom": "^20.0.2",
94
104
  "jsdom": "^24.1.0",
105
+ "lint-staged": "^15.2.11",
95
106
  "prettier": "^3.4.2",
96
107
  "rollup": "^4.55.1",
97
108
  "rollup-plugin-dts": "^6.3.0",
@@ -0,0 +1,4 @@
1
+ ## Release 1.1.0
2
+
3
+ ### Changes
4
+
package/rollup.config.js CHANGED
@@ -22,4 +22,4 @@ export default {
22
22
  }),
23
23
  ],
24
24
  external: [], // Bundle everything for now
25
- };
25
+ };
package/tsconfig.cjs.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/tsconfig",
3
- "extends": "../../tsconfig.json",
3
+ "extends": "./tsconfig.json",
4
4
  "compilerOptions": {
5
5
  "target": "ES2017",
6
6
  "module": "CommonJS",
@@ -1,42 +0,0 @@
1
- /**
2
- * A generic circular buffer implementation for storing a fixed number of items.
3
- * When the buffer is full, new items overwrite the oldest items.
4
- *
5
- * @template T The type of items stored in the buffer
6
- */
7
- export declare class CircularBuffer<T> {
8
- private maxSize;
9
- private items;
10
- private index;
11
- private count;
12
- constructor(maxSize: number);
13
- /**
14
- * Add an item to the buffer. If the buffer is full, the oldest item is overwritten.
15
- */
16
- add(item: T): void;
17
- /**
18
- * Get all items in chronological order (oldest to newest).
19
- * Returns a copy of the internal array.
20
- */
21
- getAll(): T[];
22
- /**
23
- * Clear all items from the buffer.
24
- */
25
- clear(): void;
26
- /**
27
- * Get the current number of items in the buffer.
28
- */
29
- get size(): number;
30
- /**
31
- * Get the maximum capacity of the buffer.
32
- */
33
- get capacity(): number;
34
- /**
35
- * Check if the buffer is empty.
36
- */
37
- get isEmpty(): boolean;
38
- /**
39
- * Check if the buffer is full.
40
- */
41
- get isFull(): boolean;
42
- }
@@ -1,80 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CircularBuffer = void 0;
4
- /**
5
- * A generic circular buffer implementation for storing a fixed number of items.
6
- * When the buffer is full, new items overwrite the oldest items.
7
- *
8
- * @template T The type of items stored in the buffer
9
- */
10
- class CircularBuffer {
11
- constructor(maxSize) {
12
- this.maxSize = maxSize;
13
- this.items = [];
14
- this.index = 0;
15
- this.count = 0;
16
- if (maxSize <= 0) {
17
- throw new Error('CircularBuffer maxSize must be greater than 0');
18
- }
19
- }
20
- /**
21
- * Add an item to the buffer. If the buffer is full, the oldest item is overwritten.
22
- */
23
- add(item) {
24
- if (this.count < this.maxSize) {
25
- this.items.push(item);
26
- this.count++;
27
- }
28
- else {
29
- this.items[this.index] = item;
30
- }
31
- this.index = (this.index + 1) % this.maxSize;
32
- }
33
- /**
34
- * Get all items in chronological order (oldest to newest).
35
- * Returns a copy of the internal array.
36
- */
37
- getAll() {
38
- if (this.count < this.maxSize) {
39
- return [...this.items];
40
- }
41
- // Return items in chronological order when buffer is full
42
- return [
43
- ...this.items.slice(this.index),
44
- ...this.items.slice(0, this.index),
45
- ];
46
- }
47
- /**
48
- * Clear all items from the buffer.
49
- */
50
- clear() {
51
- this.items = [];
52
- this.index = 0;
53
- this.count = 0;
54
- }
55
- /**
56
- * Get the current number of items in the buffer.
57
- */
58
- get size() {
59
- return this.count;
60
- }
61
- /**
62
- * Get the maximum capacity of the buffer.
63
- */
64
- get capacity() {
65
- return this.maxSize;
66
- }
67
- /**
68
- * Check if the buffer is empty.
69
- */
70
- get isEmpty() {
71
- return this.count === 0;
72
- }
73
- /**
74
- * Check if the buffer is full.
75
- */
76
- get isFull() {
77
- return this.count >= this.maxSize;
78
- }
79
- }
80
- exports.CircularBuffer = CircularBuffer;