@bugspotter/sdk 0.3.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +200 -0
- package/README.md +18 -16
- package/SECURITY.md +65 -0
- package/dist/bugspotter.min.js +2 -1
- package/dist/bugspotter.min.js.map +1 -0
- package/dist/capture/console.js +2 -2
- package/dist/capture/network.js +2 -2
- package/dist/core/offline-queue.d.ts +13 -0
- package/dist/core/offline-queue.js +49 -4
- package/dist/core/transport.js +20 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1460 -1178
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/utils/config-validator.js +6 -0
- package/dist/utils/sanitize-patterns.d.ts +3 -76
- package/dist/utils/sanitize-patterns.js +18 -216
- package/dist/utils/url-helpers.d.ts +2 -25
- package/dist/utils/url-helpers.js +10 -61
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/widget/button.d.ts +10 -0
- package/dist/widget/button.js +200 -3
- package/docs/CDN.md +5 -5
- package/eslint.config.js +10 -0
- package/package.json +15 -4
- package/release_notes.md +4 -0
- package/rollup.config.js +1 -1
- package/tsconfig.cjs.json +1 -1
- package/dist/core/circular-buffer.d.ts +0 -42
- package/dist/core/circular-buffer.js +0 -80
package/dist/capture/console.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConsoleCapture = exports.SDK_LOG_PREFIX = void 0;
|
|
4
4
|
const base_capture_1 = require("./base-capture");
|
|
5
|
-
const
|
|
5
|
+
const common_1 = require("@bugspotter/common");
|
|
6
6
|
const CONSOLE_METHODS = [
|
|
7
7
|
'log',
|
|
8
8
|
'warn',
|
|
@@ -21,7 +21,7 @@ class ConsoleCapture extends base_capture_1.BaseCapture {
|
|
|
21
21
|
super(options);
|
|
22
22
|
this.originalMethods = new Map();
|
|
23
23
|
const maxLogs = (_a = options.maxLogs) !== null && _a !== void 0 ? _a : 100;
|
|
24
|
-
this.buffer = new
|
|
24
|
+
this.buffer = new common_1.CircularBuffer(maxLogs);
|
|
25
25
|
this.captureStackTrace = (_b = options.captureStackTrace) !== null && _b !== void 0 ? _b : true;
|
|
26
26
|
this.interceptConsole((_c = options.levels) !== null && _c !== void 0 ? _c : CONSOLE_METHODS);
|
|
27
27
|
}
|
package/dist/capture/network.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.NetworkCapture = void 0;
|
|
4
4
|
const base_capture_1 = require("./base-capture");
|
|
5
|
-
const
|
|
5
|
+
const common_1 = require("@bugspotter/common");
|
|
6
6
|
class NetworkCapture extends base_capture_1.BaseCapture {
|
|
7
7
|
constructor(options = {}) {
|
|
8
8
|
var _a;
|
|
9
9
|
super(options);
|
|
10
10
|
this.isIntercepting = false;
|
|
11
11
|
const maxRequests = (_a = options.maxRequests) !== null && _a !== void 0 ? _a : 50;
|
|
12
|
-
this.buffer = new
|
|
12
|
+
this.buffer = new common_1.CircularBuffer(maxRequests);
|
|
13
13
|
this.filterUrls = options.filterUrls;
|
|
14
14
|
this.originalFetch = window.fetch;
|
|
15
15
|
this.originalXHR = {
|
|
@@ -32,12 +32,20 @@ export declare class OfflineQueue {
|
|
|
32
32
|
constructor(config: OfflineConfig, logger?: Logger, storage?: StorageAdapter);
|
|
33
33
|
/**
|
|
34
34
|
* Queue a request for offline retry
|
|
35
|
+
* SECURITY: Strips sensitive authentication headers before storage
|
|
35
36
|
*/
|
|
36
37
|
enqueue(endpoint: string, body: BodyInit, headers: Record<string, string>): void;
|
|
37
38
|
/**
|
|
38
39
|
* Process offline queue
|
|
40
|
+
* @deprecated Use processWithAuth() instead to properly handle authentication
|
|
39
41
|
*/
|
|
40
42
|
process(retryableStatusCodes: number[]): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Process offline queue with authentication headers
|
|
45
|
+
* @param retryableStatusCodes - HTTP status codes that should be retried
|
|
46
|
+
* @param authHeaders - Authentication headers to merge with stored headers
|
|
47
|
+
*/
|
|
48
|
+
processWithAuth(retryableStatusCodes: number[], authHeaders: Record<string, string>): Promise<void>;
|
|
41
49
|
/**
|
|
42
50
|
* Clear offline queue
|
|
43
51
|
*/
|
|
@@ -46,6 +54,11 @@ export declare class OfflineQueue {
|
|
|
46
54
|
* Get queue size
|
|
47
55
|
*/
|
|
48
56
|
size(): number;
|
|
57
|
+
/**
|
|
58
|
+
* Strip sensitive authentication headers before storing in localStorage
|
|
59
|
+
* SECURITY: Prevents API keys and tokens from being stored in plain text
|
|
60
|
+
*/
|
|
61
|
+
private stripSensitiveHeaders;
|
|
49
62
|
/**
|
|
50
63
|
* Serialize body to string format
|
|
51
64
|
*/
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.OfflineQueue = exports.LocalStorageAdapter = void 0;
|
|
7
7
|
exports.clearOfflineQueue = clearOfflineQueue;
|
|
8
8
|
const logger_1 = require("../utils/logger");
|
|
9
|
+
const url_helpers_1 = require("../utils/url-helpers");
|
|
9
10
|
/**
|
|
10
11
|
* LocalStorage implementation of StorageAdapter
|
|
11
12
|
*/
|
|
@@ -51,6 +52,18 @@ const DEFAULT_OFFLINE_CONFIG = {
|
|
|
51
52
|
enabled: false,
|
|
52
53
|
maxQueueSize: 10,
|
|
53
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Set of sensitive header names that should be stripped before localStorage storage
|
|
57
|
+
* SECURITY: Using Set for O(1) lookup performance
|
|
58
|
+
*/
|
|
59
|
+
const SENSITIVE_HEADERS = new Set([
|
|
60
|
+
'authorization',
|
|
61
|
+
'x-api-key',
|
|
62
|
+
'x-auth-token',
|
|
63
|
+
'x-access-token',
|
|
64
|
+
'cookie',
|
|
65
|
+
'set-cookie',
|
|
66
|
+
]);
|
|
54
67
|
// ============================================================================
|
|
55
68
|
// OFFLINE QUEUE CLASS
|
|
56
69
|
// ============================================================================
|
|
@@ -63,6 +76,7 @@ class OfflineQueue {
|
|
|
63
76
|
}
|
|
64
77
|
/**
|
|
65
78
|
* Queue a request for offline retry
|
|
79
|
+
* SECURITY: Strips sensitive authentication headers before storage
|
|
66
80
|
*/
|
|
67
81
|
enqueue(endpoint, body, headers) {
|
|
68
82
|
try {
|
|
@@ -76,7 +90,9 @@ class OfflineQueue {
|
|
|
76
90
|
this.logger.warn(`Offline queue is full (${this.config.maxQueueSize}), removing oldest request`);
|
|
77
91
|
queue.shift();
|
|
78
92
|
}
|
|
79
|
-
|
|
93
|
+
// SECURITY: Strip sensitive headers before storing in localStorage
|
|
94
|
+
const sanitizedHeaders = this.stripSensitiveHeaders(headers);
|
|
95
|
+
queue.push(this.createQueuedRequest(endpoint, serializedBody, sanitizedHeaders));
|
|
80
96
|
this.saveQueue(queue);
|
|
81
97
|
this.logger.log(`Request queued for offline retry (queue size: ${queue.length})`);
|
|
82
98
|
}
|
|
@@ -86,14 +102,24 @@ class OfflineQueue {
|
|
|
86
102
|
}
|
|
87
103
|
/**
|
|
88
104
|
* Process offline queue
|
|
105
|
+
* @deprecated Use processWithAuth() instead to properly handle authentication
|
|
89
106
|
*/
|
|
90
107
|
async process(retryableStatusCodes) {
|
|
108
|
+
return this.processWithAuth(retryableStatusCodes, {});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Process offline queue with authentication headers
|
|
112
|
+
* @param retryableStatusCodes - HTTP status codes that should be retried
|
|
113
|
+
* @param authHeaders - Authentication headers to merge with stored headers
|
|
114
|
+
*/
|
|
115
|
+
async processWithAuth(retryableStatusCodes, authHeaders) {
|
|
91
116
|
const queue = this.getQueue();
|
|
92
117
|
if (queue.length === 0) {
|
|
93
118
|
return;
|
|
94
119
|
}
|
|
95
120
|
this.logger.log(`Processing offline queue (${queue.length} requests)`);
|
|
96
121
|
const successfulIds = [];
|
|
122
|
+
const rejectedIds = [];
|
|
97
123
|
const failedRequests = [];
|
|
98
124
|
for (const request of queue) {
|
|
99
125
|
// Check if request has exceeded max retry attempts
|
|
@@ -109,10 +135,20 @@ class OfflineQueue {
|
|
|
109
135
|
continue;
|
|
110
136
|
}
|
|
111
137
|
try {
|
|
138
|
+
// SECURITY: Verify endpoint is secure (HTTPS) before sending
|
|
139
|
+
// This prevents downgrade attacks and ensures data confidentiality
|
|
140
|
+
if (!(0, url_helpers_1.isSecureEndpoint)(request.endpoint)) {
|
|
141
|
+
this.logger.error(`Refusing to send offline request to insecure endpoint: ${request.endpoint}`);
|
|
142
|
+
rejectedIds.push(request.id);
|
|
143
|
+
// Don't retry insecure requests
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Merge auth headers with stored headers (auth headers take precedence)
|
|
147
|
+
const headers = Object.assign(Object.assign({}, request.headers), authHeaders);
|
|
112
148
|
// Attempt to send
|
|
113
149
|
const response = await fetch(request.endpoint, {
|
|
114
150
|
method: 'POST',
|
|
115
|
-
headers
|
|
151
|
+
headers,
|
|
116
152
|
body: request.body,
|
|
117
153
|
});
|
|
118
154
|
if (response.ok) {
|
|
@@ -139,8 +175,10 @@ class OfflineQueue {
|
|
|
139
175
|
}
|
|
140
176
|
// Update queue (remove successful and expired, keep failed)
|
|
141
177
|
this.saveQueue(failedRequests);
|
|
142
|
-
if (successfulIds.length > 0 ||
|
|
143
|
-
|
|
178
|
+
if (successfulIds.length > 0 ||
|
|
179
|
+
rejectedIds.length > 0 ||
|
|
180
|
+
failedRequests.length < queue.length) {
|
|
181
|
+
this.logger.log(`Offline queue processed: ${successfulIds.length} successful, ${rejectedIds.length} rejected (insecure), ${failedRequests.length} remaining`);
|
|
144
182
|
}
|
|
145
183
|
}
|
|
146
184
|
/**
|
|
@@ -163,6 +201,13 @@ class OfflineQueue {
|
|
|
163
201
|
// ============================================================================
|
|
164
202
|
// PRIVATE METHODS
|
|
165
203
|
// ============================================================================
|
|
204
|
+
/**
|
|
205
|
+
* Strip sensitive authentication headers before storing in localStorage
|
|
206
|
+
* SECURITY: Prevents API keys and tokens from being stored in plain text
|
|
207
|
+
*/
|
|
208
|
+
stripSensitiveHeaders(headers) {
|
|
209
|
+
return Object.fromEntries(Object.entries(headers).filter(([key]) => !SENSITIVE_HEADERS.has(key.toLowerCase())));
|
|
210
|
+
}
|
|
166
211
|
/**
|
|
167
212
|
* Serialize body to string format
|
|
168
213
|
*/
|
package/dist/core/transport.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.getAuthHeaders = getAuthHeaders;
|
|
|
9
9
|
exports.submitWithAuth = submitWithAuth;
|
|
10
10
|
const logger_1 = require("../utils/logger");
|
|
11
11
|
const offline_queue_1 = require("./offline-queue");
|
|
12
|
+
const url_helpers_1 = require("../utils/url-helpers");
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// CONSTANTS
|
|
14
15
|
// ============================================================================
|
|
@@ -109,8 +110,8 @@ class RetryHandler {
|
|
|
109
110
|
calculateDelay(attempt, response) {
|
|
110
111
|
var _a, _b;
|
|
111
112
|
// Check for Retry-After header
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
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');
|
|
114
|
+
if (retryAfter) {
|
|
114
115
|
const retryAfterSeconds = parseInt(retryAfter, 10);
|
|
115
116
|
if (!isNaN(retryAfterSeconds)) {
|
|
116
117
|
return Math.min(retryAfterSeconds * 1000, this.config.maxDelay);
|
|
@@ -131,26 +132,30 @@ class RetryHandler {
|
|
|
131
132
|
/**
|
|
132
133
|
* Process offline queue in background
|
|
133
134
|
*/
|
|
134
|
-
async function processQueueInBackground(offlineConfig, retryConfig, logger) {
|
|
135
|
+
async function processQueueInBackground(offlineConfig, retryConfig, auth, logger) {
|
|
135
136
|
if (!offlineConfig.enabled) {
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
139
|
const queue = new offline_queue_1.OfflineQueue(offlineConfig, logger);
|
|
139
|
-
|
|
140
|
+
const authHeaders = generateAuthHeaders(auth);
|
|
141
|
+
queue
|
|
142
|
+
.processWithAuth(retryConfig.retryOn, authHeaders)
|
|
143
|
+
.catch((error) => {
|
|
140
144
|
logger.warn('Failed to process offline queue:', error);
|
|
141
145
|
});
|
|
142
146
|
}
|
|
143
147
|
/**
|
|
144
148
|
* Handle offline failure by queueing request
|
|
149
|
+
* SECURITY: Does not pass auth headers to queue - they will be regenerated when processing
|
|
145
150
|
*/
|
|
146
|
-
async function handleOfflineFailure(error, endpoint, body, contentHeaders,
|
|
151
|
+
async function handleOfflineFailure(error, endpoint, body, contentHeaders, _auth, offlineConfig, logger) {
|
|
147
152
|
if (!offlineConfig.enabled || !isNetworkError(error)) {
|
|
148
153
|
return;
|
|
149
154
|
}
|
|
150
155
|
logger.warn('Network error detected, queueing request for offline retry');
|
|
151
156
|
const queue = new offline_queue_1.OfflineQueue(offlineConfig, logger);
|
|
152
|
-
|
|
153
|
-
await queue.enqueue(endpoint, body,
|
|
157
|
+
// SECURITY: Only pass content headers, not auth headers - auth will be regenerated when processing
|
|
158
|
+
await queue.enqueue(endpoint, body, contentHeaders);
|
|
154
159
|
}
|
|
155
160
|
// ============================================================================
|
|
156
161
|
// PUBLIC API
|
|
@@ -176,8 +181,12 @@ async function submitWithAuth(endpoint, body, contentHeaders = {}, options) {
|
|
|
176
181
|
const logger = options.logger || (0, logger_1.getLogger)();
|
|
177
182
|
const retryConfig = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), options.retry);
|
|
178
183
|
const offlineConfig = Object.assign(Object.assign({}, DEFAULT_OFFLINE_CONFIG), options.offline);
|
|
184
|
+
// Security: Enforce HTTPS
|
|
185
|
+
if (!(0, url_helpers_1.isSecureEndpoint)(endpoint)) {
|
|
186
|
+
throw new url_helpers_1.InsecureEndpointError(endpoint);
|
|
187
|
+
}
|
|
179
188
|
// Process offline queue on each request (run in background without awaiting)
|
|
180
|
-
processQueueInBackground(offlineConfig, retryConfig, logger);
|
|
189
|
+
processQueueInBackground(offlineConfig, retryConfig, options.auth, logger);
|
|
181
190
|
try {
|
|
182
191
|
// Send with retry logic
|
|
183
192
|
const response = await sendWithRetry(endpoint, body, contentHeaders, options.auth, retryConfig, logger);
|
|
@@ -196,6 +205,9 @@ async function submitWithAuth(endpoint, body, contentHeaders = {}, options) {
|
|
|
196
205
|
* Make HTTP request with auth headers
|
|
197
206
|
*/
|
|
198
207
|
async function makeRequest(endpoint, body, contentHeaders, auth) {
|
|
208
|
+
if (!(0, url_helpers_1.isSecureEndpoint)(endpoint)) {
|
|
209
|
+
throw new url_helpers_1.InsecureEndpointError(endpoint);
|
|
210
|
+
}
|
|
199
211
|
const authHeaders = generateAuthHeaders(auth);
|
|
200
212
|
const headers = Object.assign(Object.assign({}, contentHeaders), authHeaders);
|
|
201
213
|
return fetch(endpoint, {
|
package/dist/index.d.ts
CHANGED