securenow 5.9.0 → 5.10.1

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.
@@ -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,
@@ -54,7 +54,7 @@ async function safeBodyCapture(request, span) {
54
54
 
55
55
  try {
56
56
  const contentType = request.headers.get('content-type') || '';
57
- const maxBodySize = parseInt(process.env.SECURENOW_MAX_BODY_SIZE || '10240');
57
+ const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
58
58
  const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
59
59
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
60
60
 
@@ -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
 
@@ -99,7 +99,7 @@ async function middleware(request) {
99
99
 
100
100
  try {
101
101
  const contentType = request.headers.get('content-type') || '';
102
- const maxBodySize = parseInt(process.env.SECURENOW_MAX_BODY_SIZE || '10240');
102
+ const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
103
103
  const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
104
104
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
105
105
 
@@ -124,7 +124,7 @@ async function middleware(request) {
124
124
  const redacted = redactSensitiveData(parsed, allSensitiveFields);
125
125
  redactedBody = JSON.stringify(redacted);
126
126
  } catch (e) {
127
- redactedBody = bodyText; // Keep as-is if parse fails
127
+ redactedBody = '[UNPARSEABLE - REDACTED FOR SAFETY]';
128
128
  }
129
129
  }
130
130
 
package/nextjs-wrapper.js CHANGED
@@ -60,7 +60,7 @@ async function captureRequestBody(request) {
60
60
 
61
61
  try {
62
62
  const contentType = request.headers.get('content-type') || '';
63
- const maxBodySize = parseInt(process.env.SECURENOW_MAX_BODY_SIZE || '10240');
63
+ const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
64
64
  const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
65
65
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
66
66
 
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.9.0",
3
+ "version": "5.10.1",
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
@@ -370,7 +370,7 @@ function resolveClientIp(request) {
370
370
  for (let i = chain.length - 1; i >= 0; i--) {
371
371
  if (!isFromTrustedProxy(chain[i])) return chain[i];
372
372
  }
373
- return chain[0] || socketIp;
373
+ return socketIp;
374
374
  }
375
375
  const headerIp = request.headers['x-real-ip'];
376
376
  if (headerIp) return headerIp;
@@ -394,12 +394,12 @@ const httpInstrumentation = new HttpInstrumentation({
394
394
  span.setAttribute('http.client_ip', clientIp);
395
395
  }
396
396
 
397
- if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
397
+ if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
398
398
  const contentType = request.headers['content-type'] || '';
399
399
 
400
- if (contentType.includes('application/json') ||
400
+ if (captureBody && (contentType.includes('application/json') ||
401
401
  contentType.includes('application/graphql') ||
402
- contentType.includes('application/x-www-form-urlencoded')) {
402
+ contentType.includes('application/x-www-form-urlencoded'))) {
403
403
 
404
404
  let body = '';
405
405
  const chunks = [];
@@ -545,6 +545,7 @@ if (loggingEnabled) {
545
545
  console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
546
546
  console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
547
547
  console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
548
+ console.__securenow_patched = true;
548
549
  }
549
550
 
550
551
  // -------- SDK --------
package/web-vite.mjs CHANGED
@@ -72,20 +72,19 @@ function uuidv4(): string {
72
72
  }
73
73
 
74
74
  let serviceName: string;
75
+ let disabled = false;
75
76
  if (baseName) {
76
77
  serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
77
78
  } else {
78
79
  if (strict) {
79
80
  console.error('[securenow/web-vite] FATAL: SECURENOW_APPID/OTEL_SERVICE_NAME missing and SECURENOW_STRICT=1. Tracing disabled.');
80
- // Do not start tracing
81
81
  // @ts-expect-error
82
82
  window.__SECURENOW_DISABLED__ = true;
83
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
84
- const _noop = true;
85
- // early return by throwing a no-op error caught below:
86
- throw new Error('__SECURENOW_NO_START__');
83
+ disabled = true;
84
+ serviceName = 'disabled';
85
+ } else {
86
+ serviceName = `securenow-free-${uuidv4()}`;
87
87
  }
88
- serviceName = `securenow-free-${uuidv4()}`;
89
88
  }
90
89
 
91
90
  const instancePrefix = baseName || 'securenow';
@@ -109,7 +108,7 @@ try {
109
108
  let started = false;
110
109
 
111
110
  export function startSecurenowWeb() {
112
- if (started) return;
111
+ if (started || disabled) return;
113
112
  started = true;
114
113
 
115
114
  const exporter = new OTLPTraceExporter({
@@ -145,9 +144,10 @@ export function startSecurenowWeb() {
145
144
 
146
145
  // Optional smoke span (same flag name)
147
146
  if (String(env('SECURENOW_TEST_SPAN')) === '1') {
148
- const api = await import('@opentelemetry/api');
149
- const tracer = api.trace.getTracer('securenow-smoke');
150
- 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(() => {});
151
151
  }
152
152
 
153
153
  // eslint-disable-next-line no-console
@@ -233,9 +233,7 @@ try {
233
233
  startSecurenowWeb();
234
234
  injectFreeTrialBanner();
235
235
  } catch (e: any) {
236
- if (String(e?.message) !== '__SECURENOW_NO_START__') {
237
- console.error('[securenow/web-vite] failed to start:', e);
238
- }
236
+ console.error('[securenow/web-vite] failed to start:', e);
239
237
  }
240
238
 
241
239
  export default startSecurenowWeb;