@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.
@@ -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 circular_buffer_1 = require("../core/circular-buffer");
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 circular_buffer_1.CircularBuffer(maxLogs);
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
  }
@@ -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 circular_buffer_1 = require("../core/circular-buffer");
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 circular_buffer_1.CircularBuffer(maxRequests);
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
- queue.push(this.createQueuedRequest(endpoint, serializedBody, headers));
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: request.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 || failedRequests.length < queue.length) {
143
- this.logger.log(`Offline queue processed: ${successfulIds.length} successful, ${failedRequests.length} remaining`);
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
  */
@@ -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
- 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')) {
113
- const retryAfter = response.headers.get('Retry-After');
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
- queue.process(retryConfig.retryOn).catch((error) => {
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, auth, offlineConfig, logger) {
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
- const authHeaders = generateAuthHeaders(auth);
153
- await queue.enqueue(endpoint, body, Object.assign(Object.assign({}, contentHeaders), authHeaders));
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
@@ -167,3 +167,4 @@ export { DEFAULT_REPLAY_DURATION_SECONDS, MAX_RECOMMENDED_REPLAY_DURATION_SECOND
167
167
  * ```
168
168
  */
169
169
  export declare function sanitize(text: string): string;
170
+ export default BugSpotter;