securenow 5.8.1 → 5.10.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/cli/auth.js CHANGED
@@ -10,7 +10,7 @@ function openBrowser(url) {
10
10
  try {
11
11
  const platform = process.platform;
12
12
  if (platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
13
- else if (platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
13
+ else if (platform === 'win32') execFileSync('rundll32', ['url.dll,FileProtocolHandler', url], { stdio: 'ignore' });
14
14
  else execFileSync('xdg-open', [url], { stdio: 'ignore' });
15
15
  return true;
16
16
  } catch {
package/cli/config.js CHANGED
@@ -32,6 +32,12 @@ function loadJSON(filepath) {
32
32
  function saveJSON(filepath, data) {
33
33
  ensureDir();
34
34
  fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
35
+ if (process.platform === 'win32') {
36
+ try {
37
+ const { execFileSync } = require('child_process');
38
+ execFileSync('icacls', [filepath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`], { stdio: 'ignore' });
39
+ } catch (_) {}
40
+ }
35
41
  }
36
42
 
37
43
  function loadConfig() {
@@ -36,6 +36,12 @@ if (!logger) {
36
36
  return;
37
37
  }
38
38
 
39
+ if (console.__securenow_patched) {
40
+ console.warn('[securenow] Console already instrumented by tracing.js — skipping to avoid duplicate logs.');
41
+ module.exports = {};
42
+ return;
43
+ }
44
+
39
45
  // Store original console methods
40
46
  const originalConsole = {
41
47
  log: console.log,
@@ -107,7 +107,8 @@ function patchHttpForBanner() {
107
107
  if (res._snIsHtml === undefined) {
108
108
  var ct = res.getHeader('content-type');
109
109
  var ce = res.getHeader('content-encoding');
110
- res._snIsHtml = !!(ct && String(ct).includes('text/html') && !ce);
110
+ var csp = res.getHeader('content-security-policy');
111
+ res._snIsHtml = !!(ct && String(ct).includes('text/html') && !ce && !csp);
111
112
  }
112
113
  if (!res._snIsHtml) {
113
114
  res._snBannerDone = true;
@@ -136,6 +137,11 @@ function patchHttpForBanner() {
136
137
  if (modified !== chunk) {
137
138
  var enc = typeof encoding === 'function' ? 'utf8' : encoding;
138
139
  var callback = typeof encoding === 'function' ? encoding : cb;
140
+ try {
141
+ if (this.getHeader('content-length')) {
142
+ this.setHeader('content-length', Buffer.byteLength(modified));
143
+ }
144
+ } catch (_) { /* headers already sent */ }
139
145
  return _origWrite.call(this, modified, enc, callback);
140
146
  }
141
147
  } catch (_) { /* never break the app */ }
@@ -33,7 +33,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
33
33
 
34
34
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
35
35
 
36
- for (const key in redacted) {
36
+ for (const key of Object.keys(redacted)) {
37
37
  const lowerKey = key.toLowerCase();
38
38
 
39
39
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
@@ -139,7 +139,6 @@ function patchNextRequest() {
139
139
 
140
140
  const originalText = Request.prototype.text;
141
141
  const originalJson = Request.prototype.json;
142
- const originalFormData = Request.prototype.formData;
143
142
 
144
143
  // Patch text() to cache result
145
144
  Request.prototype.text = async function() {
@@ -171,13 +170,6 @@ function patchNextRequest() {
171
170
  return JSON.parse(text);
172
171
  };
173
172
 
174
- // Patch formData() to cache and capture
175
- Request.prototype.formData = async function() {
176
- const text = await this.text();
177
- const params = new URLSearchParams(text);
178
- return params;
179
- };
180
-
181
173
  console.log('[securenow] ✅ Auto-capture: Patched Next.js Request for automatic body capture');
182
174
  }
183
175
 
@@ -27,12 +27,16 @@ const DEFAULT_SENSITIVE_FIELDS = [
27
27
  /**
28
28
  * Redact sensitive fields from an object
29
29
  */
30
+ function escapeRegex(str) {
31
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
30
34
  function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
31
35
  if (!obj || typeof obj !== 'object') return obj;
32
36
 
33
37
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
34
38
 
35
- for (const key in redacted) {
39
+ for (const key of Object.keys(redacted)) {
36
40
  const lowerKey = key.toLowerCase();
37
41
 
38
42
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
@@ -54,9 +58,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
54
58
  let redacted = query;
55
59
 
56
60
  sensitiveFields.forEach(field => {
61
+ const escaped = escapeRegex(field);
57
62
  const patterns = [
58
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
59
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
63
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
64
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
60
65
  ];
61
66
 
62
67
  patterns.forEach(pattern => {
package/nextjs-wrapper.js CHANGED
@@ -32,7 +32,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
32
32
 
33
33
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
34
34
 
35
- for (const key in redacted) {
35
+ for (const key of Object.keys(redacted)) {
36
36
  const lowerKey = key.toLowerCase();
37
37
 
38
38
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
package/nextjs.js CHANGED
@@ -68,10 +68,9 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
68
68
 
69
69
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
70
70
 
71
- for (const key in redacted) {
71
+ for (const key of Object.keys(redacted)) {
72
72
  const lowerKey = key.toLowerCase();
73
73
 
74
- // Check if field is sensitive
75
74
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
76
75
  redacted[key] = '[REDACTED]';
77
76
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
@@ -83,6 +82,10 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
83
82
  return redacted;
84
83
  }
85
84
 
85
+ function escapeRegex(str) {
86
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
87
+ }
88
+
86
89
  /**
87
90
  * Redact sensitive data from GraphQL query strings
88
91
  */
@@ -94,10 +97,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
94
97
  // Redact sensitive fields in GraphQL arguments and variables
95
98
  // Matches patterns like: password: "value" or password:"value" or password:'value'
96
99
  sensitiveFields.forEach(field => {
97
- // Match field: "value" or field: 'value' or field:"value" (with optional spaces)
100
+ const escaped = escapeRegex(field);
98
101
  const patterns = [
99
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
100
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
102
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
103
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
101
104
  ];
102
105
 
103
106
  patterns.forEach(pattern => {
@@ -114,115 +117,6 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
114
117
  return redacted;
115
118
  }
116
119
 
117
- /**
118
- * Parse and capture request body safely
119
- */
120
- async function captureRequestBody(request, maxSize = 10240) {
121
- try {
122
- const contentType = request.headers['content-type'] || '';
123
- let body = '';
124
-
125
- // Collect body chunks
126
- const chunks = [];
127
- let size = 0;
128
-
129
- return new Promise((resolve) => {
130
- request.on('data', (chunk) => {
131
- size += chunk.length;
132
- if (size <= maxSize) {
133
- chunks.push(chunk);
134
- }
135
- });
136
-
137
- request.on('end', () => {
138
- if (size > maxSize) {
139
- resolve({
140
- captured: false,
141
- reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
142
- size
143
- });
144
- return;
145
- }
146
-
147
- body = Buffer.concat(chunks).toString('utf8');
148
-
149
- // Parse based on content type
150
- if (contentType.includes('application/json')) {
151
- try {
152
- const parsed = JSON.parse(body);
153
- resolve({
154
- captured: true,
155
- type: 'json',
156
- body: parsed,
157
- size
158
- });
159
- } catch (e) {
160
- resolve({
161
- captured: true,
162
- type: 'json',
163
- body: body.substring(0, 1000),
164
- parseError: true,
165
- size
166
- });
167
- }
168
- } else if (contentType.includes('application/graphql')) {
169
- // GraphQL queries need redaction too!
170
- resolve({
171
- captured: true,
172
- type: 'graphql',
173
- body: body, // Will be redacted later
174
- size
175
- });
176
- } else if (contentType.includes('multipart/form-data')) {
177
- // Multipart is NOT captured (files can be huge)
178
- resolve({
179
- captured: false,
180
- type: 'multipart',
181
- reason: 'Multipart data not captured (file uploads)',
182
- size
183
- });
184
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
185
- try {
186
- const params = new URLSearchParams(body);
187
- const parsed = Object.fromEntries(params);
188
- resolve({
189
- captured: true,
190
- type: 'form',
191
- body: parsed,
192
- size
193
- });
194
- } catch (e) {
195
- resolve({
196
- captured: true,
197
- type: 'form',
198
- body: body.substring(0, 1000),
199
- size
200
- });
201
- }
202
- } else {
203
- resolve({
204
- captured: true,
205
- type: 'text',
206
- body: body.substring(0, 1000),
207
- size
208
- });
209
- }
210
- });
211
-
212
- request.on('error', () => {
213
- resolve({ captured: false, reason: 'Stream error' });
214
- });
215
-
216
- // Timeout after 100ms
217
- setTimeout(() => {
218
- resolve({ captured: false, reason: 'Timeout' });
219
- }, 100);
220
- });
221
- } catch (error) {
222
- return { captured: false, reason: error.message };
223
- }
224
- }
225
-
226
120
  /**
227
121
  * Register SecureNow OpenTelemetry for Next.js using @vercel/otel
228
122
  * @param {Object} options - Optional configuration
@@ -281,10 +175,9 @@ function registerSecureNow(options = {}) {
281
175
  const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
282
176
  const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
283
177
 
284
- // Set environment variables for @vercel/otel to pick up
285
- process.env.OTEL_SERVICE_NAME = serviceName;
286
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
287
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
178
+ if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
179
+ if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
180
+ if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
288
181
 
289
182
  console.log('[securenow] 🚀 Next.js App → service.name=%s', serviceName);
290
183
 
@@ -292,7 +185,7 @@ function registerSecureNow(options = {}) {
292
185
  const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
293
186
  String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
294
187
  options.captureBody === true;
295
- const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
188
+ const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
296
189
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
297
190
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
298
191
 
@@ -326,14 +219,19 @@ function registerSecureNow(options = {}) {
326
219
  const clientIp = headers['x-client-ip'];
327
220
  const socketIp = request.socket?.remoteAddress;
328
221
 
329
- // Primary IP (first in chain is the real client)
330
- const primaryIp =
331
- (forwardedFor ? forwardedFor.split(',')[0]?.trim() : null) ||
332
- realIp ||
333
- cfConnectingIp ||
334
- clientIp ||
335
- socketIp ||
336
- 'unknown';
222
+ const PRIVATE_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
223
+ const isProxied = socketIp && PRIVATE_RE.test(socketIp);
224
+ let primaryIp = socketIp || 'unknown';
225
+ if (isProxied) {
226
+ if (forwardedFor) {
227
+ const chain = forwardedFor.split(',').map(s => s.trim()).filter(Boolean);
228
+ for (let i = chain.length - 1; i >= 0; i--) {
229
+ if (!PRIVATE_RE.test(chain[i])) { primaryIp = chain[i]; break; }
230
+ }
231
+ } else {
232
+ primaryIp = realIp || cfConnectingIp || clientIp || primaryIp;
233
+ }
234
+ }
337
235
 
338
236
  // ======== PROTOCOL & CONNECTION ========
339
237
  const scheme = headers['x-forwarded-proto'] ||
@@ -588,10 +486,9 @@ function registerSecureNow(options = {}) {
588
486
  const start = Date.now();
589
487
  const method = req.method;
590
488
  const url = req.url;
591
- const reqCtx = otelContext.active();
592
- const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
593
-
594
489
  res.on('finish', () => {
490
+ const reqCtx = otelContext.active();
491
+ const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
595
492
  const duration = Date.now() - start;
596
493
  const status = res.statusCode;
597
494
  const ip = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket?.remoteAddress || '-';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.8.1",
3
+ "version": "5.10.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
package/tracing.js CHANGED
@@ -53,6 +53,10 @@ const DEFAULT_SENSITIVE_FIELDS = [
53
53
  'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
54
54
  ];
55
55
 
56
+ function escapeRegex(str) {
57
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ }
59
+
56
60
  /**
57
61
  * Redact sensitive fields from an object
58
62
  */
@@ -61,7 +65,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
61
65
 
62
66
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
63
67
 
64
- for (const key in redacted) {
68
+ for (const key of Object.keys(redacted)) {
65
69
  const lowerKey = key.toLowerCase();
66
70
 
67
71
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
@@ -84,10 +88,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
84
88
 
85
89
  // Redact sensitive fields in GraphQL arguments and variables
86
90
  sensitiveFields.forEach(field => {
87
- // Match patterns: field: "value" or field: 'value' or field:"value"
91
+ const escaped = escapeRegex(field);
88
92
  const patterns = [
89
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
90
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
93
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
94
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
91
95
  ];
92
96
 
93
97
  patterns.forEach(pattern => {
@@ -118,7 +122,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
118
122
  const boundary = extractBoundary(contentType);
119
123
  if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
120
124
 
121
- const result = { fields: {}, files: [] };
125
+ const result = { fields: Object.create(null), files: [] };
122
126
  let totalSize = 0;
123
127
  let buf = Buffer.alloc(0);
124
128
 
@@ -139,7 +143,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
139
143
  let textVal = '';
140
144
 
141
145
  function flushPart() {
142
- if (!fldName) return;
146
+ if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
143
147
  if (isFile) {
144
148
  result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
145
149
  } else {
@@ -316,7 +320,7 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
316
320
 
317
321
  // -------- Body Capture Configuration --------
318
322
  const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
319
- const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
323
+ const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
320
324
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
321
325
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
322
326
 
@@ -328,7 +332,7 @@ const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || S
328
332
  // This prevents end-users from spoofing their IP via custom headers.
329
333
  const os = require('os');
330
334
  const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
331
- const PRIVATE_IP_RE = /^(127\.|::1|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|fc|fd)/;
335
+ const PRIVATE_IP_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
332
336
  const trustedProxyCsv = (env('SECURENOW_TRUSTED_PROXIES') || '').trim();
333
337
  const trustedProxySet = trustedProxyCsv ? new Set(trustedProxyCsv.split(',').map(s => s.trim()).filter(Boolean)) : null;
334
338
 
@@ -366,7 +370,7 @@ function resolveClientIp(request) {
366
370
  for (let i = chain.length - 1; i >= 0; i--) {
367
371
  if (!isFromTrustedProxy(chain[i])) return chain[i];
368
372
  }
369
- return chain[0] || socketIp;
373
+ return socketIp;
370
374
  }
371
375
  const headerIp = request.headers['x-real-ip'];
372
376
  if (headerIp) return headerIp;
@@ -390,12 +394,12 @@ const httpInstrumentation = new HttpInstrumentation({
390
394
  span.setAttribute('http.client_ip', clientIp);
391
395
  }
392
396
 
393
- if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
397
+ if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
394
398
  const contentType = request.headers['content-type'] || '';
395
399
 
396
- if (contentType.includes('application/json') ||
400
+ if (captureBody && (contentType.includes('application/json') ||
397
401
  contentType.includes('application/graphql') ||
398
- contentType.includes('application/x-www-form-urlencoded')) {
402
+ contentType.includes('application/x-www-form-urlencoded'))) {
399
403
 
400
404
  let body = '';
401
405
  const chunks = [];
@@ -443,9 +447,9 @@ const httpInstrumentation = new HttpInstrumentation({
443
447
  });
444
448
  }
445
449
  } catch (e) {
446
- // Parse error: capture as-is (truncated)
447
- span.setAttribute('http.request.body', body.substring(0, 1000));
450
+ span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
448
451
  span.setAttribute('http.request.body.parse_error', true);
452
+ span.setAttribute('http.request.body.size', size);
449
453
  }
450
454
  } else if (size > maxBodySize) {
451
455
  span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
@@ -541,6 +545,7 @@ if (loggingEnabled) {
541
545
  console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
542
546
  console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
543
547
  console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
548
+ console.__securenow_patched = true;
544
549
  }
545
550
 
546
551
  // -------- SDK --------
package/web-vite.mjs CHANGED
@@ -55,27 +55,36 @@ const baseName = rawBase || null;
55
55
  const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
56
56
  const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
57
57
 
58
- // Simple UUID v4 (no crypto dependency needed)
59
58
  function uuidv4(): string {
60
- const rnd = (n = 16) => Array.from({ length: n }, () => Math.floor(Math.random() * 16).toString(16)).join('');
61
- return `${rnd(8)}-${rnd(4)}-4${rnd(3)}-${((8 + Math.random()*4)|0).toString(16)}${rnd(3)}-${rnd(12)}`;
59
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
60
+ return crypto.randomUUID();
61
+ }
62
+ const bytes = new Uint8Array(16);
63
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
64
+ crypto.getRandomValues(bytes);
65
+ } else {
66
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
67
+ }
68
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
69
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
70
+ const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
71
+ return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
62
72
  }
63
73
 
64
74
  let serviceName: string;
75
+ let disabled = false;
65
76
  if (baseName) {
66
77
  serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
67
78
  } else {
68
79
  if (strict) {
69
80
  console.error('[securenow/web-vite] FATAL: SECURENOW_APPID/OTEL_SERVICE_NAME missing and SECURENOW_STRICT=1. Tracing disabled.');
70
- // Do not start tracing
71
81
  // @ts-expect-error
72
82
  window.__SECURENOW_DISABLED__ = true;
73
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
74
- const _noop = true;
75
- // early return by throwing a no-op error caught below:
76
- throw new Error('__SECURENOW_NO_START__');
83
+ disabled = true;
84
+ serviceName = 'disabled';
85
+ } else {
86
+ serviceName = `securenow-free-${uuidv4()}`;
77
87
  }
78
- serviceName = `securenow-free-${uuidv4()}`;
79
88
  }
80
89
 
81
90
  const instancePrefix = baseName || 'securenow';
@@ -99,7 +108,7 @@ try {
99
108
  let started = false;
100
109
 
101
110
  export function startSecurenowWeb() {
102
- if (started) return;
111
+ if (started || disabled) return;
103
112
  started = true;
104
113
 
105
114
  const exporter = new OTLPTraceExporter({
@@ -124,20 +133,21 @@ export function startSecurenowWeb() {
124
133
  new DocumentLoadInstrumentation(),
125
134
  new UserInteractionInstrumentation(),
126
135
  new FetchInstrumentation({
127
- propagateTraceHeaderCorsUrls: [/.*/],
136
+ propagateTraceHeaderCorsUrls: [new RegExp(`^${location.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
128
137
  ignoreUrls: [/\/vite\/hmr/, /^chrome-extension:\/\//, /sockjs/],
129
138
  }),
130
139
  new XMLHttpRequestInstrumentation({
131
- propagateTraceHeaderCorsUrls: [/.*/],
140
+ propagateTraceHeaderCorsUrls: [new RegExp(`^${location.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
132
141
  }),
133
142
  ],
134
143
  });
135
144
 
136
145
  // Optional smoke span (same flag name)
137
146
  if (String(env('SECURENOW_TEST_SPAN')) === '1') {
138
- const api = await import('@opentelemetry/api');
139
- const tracer = api.trace.getTracer('securenow-smoke');
140
- const span = tracer.startSpan('securenow.startup.smoke.web'); span.end();
147
+ import('@opentelemetry/api').then(api => {
148
+ const tracer = api.trace.getTracer('securenow-smoke');
149
+ const span = tracer.startSpan('securenow.startup.smoke.web'); span.end();
150
+ }).catch(() => {});
141
151
  }
142
152
 
143
153
  // eslint-disable-next-line no-console
@@ -223,9 +233,7 @@ try {
223
233
  startSecurenowWeb();
224
234
  injectFreeTrialBanner();
225
235
  } catch (e: any) {
226
- if (String(e?.message) !== '__SECURENOW_NO_START__') {
227
- console.error('[securenow/web-vite] failed to start:', e);
228
- }
236
+ console.error('[securenow/web-vite] failed to start:', e);
229
237
  }
230
238
 
231
239
  export default startSecurenowWeb;