securenow 6.0.2 โ†’ 6.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.
Files changed (87) hide show
  1. package/CONSUMING-APPS-GUIDE.md +455 -0
  2. package/NPM_README.md +2029 -0
  3. package/README.md +297 -40
  4. package/SKILL-API.md +634 -0
  5. package/SKILL-CLI.md +454 -0
  6. package/cidr.js +83 -0
  7. package/cli/apps.js +585 -0
  8. package/cli/auth.js +280 -0
  9. package/cli/client.js +115 -0
  10. package/cli/config.js +173 -0
  11. package/cli/diagnostics.js +387 -0
  12. package/cli/firewall.js +100 -0
  13. package/cli/fp.js +638 -0
  14. package/cli/init.js +201 -0
  15. package/cli/monitor.js +440 -0
  16. package/cli/run.js +148 -0
  17. package/cli/security.js +980 -0
  18. package/cli/ui.js +386 -0
  19. package/cli/utils.js +127 -0
  20. package/cli.js +466 -455
  21. package/console-instrumentation.js +147 -136
  22. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
  23. package/docs/API-KEYS-GUIDE.md +233 -0
  24. package/docs/ARCHITECTURE.md +3 -3
  25. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  26. package/docs/AUTO-SETUP-SUMMARY.md +331 -0
  27. package/docs/AUTO-SETUP.md +4 -4
  28. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  29. package/docs/BODY-CAPTURE-FIX.md +261 -0
  30. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  31. package/docs/CHANGELOG-NEXTJS.md +1 -35
  32. package/docs/COMPLETION-REPORT.md +408 -0
  33. package/docs/CUSTOMER-GUIDE.md +16 -16
  34. package/docs/EASIEST-SETUP.md +5 -5
  35. package/docs/ENVIRONMENT-VARIABLES.md +880 -652
  36. package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
  37. package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
  38. package/docs/FINAL-SOLUTION.md +335 -0
  39. package/docs/FIREWALL-GUIDE.md +426 -0
  40. package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
  41. package/docs/INDEX.md +22 -4
  42. package/docs/LOGGING-GUIDE.md +701 -708
  43. package/docs/LOGGING-QUICKSTART.md +234 -255
  44. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
  45. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  46. package/docs/NEXTJS-GUIDE.md +14 -14
  47. package/docs/NEXTJS-QUICKSTART.md +1 -1
  48. package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
  49. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  50. package/docs/NUXT-GUIDE.md +166 -0
  51. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  52. package/docs/REDACTION-EXAMPLES.md +1 -1
  53. package/docs/REQUEST-BODY-CAPTURE.md +19 -10
  54. package/docs/SOLUTION-SUMMARY.md +312 -0
  55. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  56. package/examples/README.md +6 -6
  57. package/examples/instrumentation-with-auto-capture.ts +1 -1
  58. package/examples/nextjs-env-example.txt +2 -2
  59. package/examples/nextjs-instrumentation.js +1 -1
  60. package/examples/nextjs-instrumentation.ts +1 -1
  61. package/examples/nextjs-with-logging-example.md +6 -6
  62. package/examples/nextjs-with-options.ts +1 -1
  63. package/examples/test-nextjs-setup.js +1 -1
  64. package/firewall-cloud.js +212 -0
  65. package/firewall-iptables.js +139 -0
  66. package/firewall-only.js +38 -0
  67. package/firewall-tcp.js +74 -0
  68. package/firewall.js +720 -0
  69. package/free-trial-banner.js +174 -0
  70. package/nextjs-auto-capture.js +199 -207
  71. package/nextjs-middleware.js +186 -181
  72. package/nextjs-webpack-config.js +88 -53
  73. package/nextjs-wrapper.js +158 -158
  74. package/nextjs.d.ts +1 -1
  75. package/nextjs.js +639 -647
  76. package/nuxt-server-plugin.mjs +423 -0
  77. package/nuxt.d.ts +60 -0
  78. package/nuxt.mjs +75 -0
  79. package/package.json +186 -164
  80. package/postinstall.js +6 -6
  81. package/register.d.ts +1 -1
  82. package/register.js +39 -4
  83. package/resolve-ip.js +77 -0
  84. package/tracing.d.ts +2 -1
  85. package/tracing.js +295 -34
  86. package/web-vite.mjs +239 -156
  87. package/LICENSE +0 -15
package/nextjs.js CHANGED
@@ -1,647 +1,639 @@
1
- 'use strict';
2
-
3
- /**
4
- * SecureNow Next.js Integration using @vercel/otel
5
- *
6
- * Usage in Next.js app:
7
- *
8
- * 1. Create instrumentation.ts (or .js) in your project root:
9
- *
10
- * import { registerSecureNow } from 'securenow/nextjs';
11
- * export function register() {
12
- * registerSecureNow();
13
- * }
14
- *
15
- * 2. Set environment variables:
16
- * SECURENOW_APPID=my-nextjs-app
17
- * SECURENOW_INSTANCE=http://your-signoz-host:4318
18
- *
19
- * That's it! ๐ŸŽ‰ No webpack warnings!
20
- */
21
-
22
- const { v4: uuidv4 } = require('uuid');
23
-
24
- const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
25
-
26
- const parseHeaders = str => {
27
- const out = {}; if (!str) return out;
28
- for (const raw of String(str).split(',')) {
29
- const s = raw.trim(); if (!s) continue;
30
- const i = s.indexOf('='); if (i === -1) continue;
31
- out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
32
- }
33
- return out;
34
- };
35
-
36
- let isRegistered = false;
37
-
38
- // Default sensitive fields to redact from request bodies
39
- const DEFAULT_SENSITIVE_FIELDS = [
40
- 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
41
- 'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
42
- 'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
43
- ];
44
-
45
- /**
46
- * Redact sensitive fields from an object
47
- */
48
- function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
49
- if (!obj || typeof obj !== 'object') return obj;
50
-
51
- const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
52
-
53
- for (const key in redacted) {
54
- const lowerKey = key.toLowerCase();
55
-
56
- // Check if field is sensitive
57
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
58
- redacted[key] = '[REDACTED]';
59
- } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
60
- // Recursively redact nested objects
61
- redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
62
- }
63
- }
64
-
65
- return redacted;
66
- }
67
-
68
- /**
69
- * Redact sensitive data from GraphQL query strings
70
- */
71
- function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
72
- if (!query || typeof query !== 'string') return query;
73
-
74
- let redacted = query;
75
-
76
- // Redact sensitive fields in GraphQL arguments and variables
77
- // Matches patterns like: password: "value" or password:"value" or password:'value'
78
- sensitiveFields.forEach(field => {
79
- // Match field: "value" or field: 'value' or field:"value" (with optional spaces)
80
- const patterns = [
81
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
82
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
83
- ];
84
-
85
- patterns.forEach(pattern => {
86
- redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
87
- if (suffix) {
88
- return `${prefix}[REDACTED]${suffix}`;
89
- } else {
90
- return `${prefix}[REDACTED]`;
91
- }
92
- });
93
- });
94
- });
95
-
96
- return redacted;
97
- }
98
-
99
- /**
100
- * Parse and capture request body safely
101
- */
102
- async function captureRequestBody(request, maxSize = 10240) {
103
- try {
104
- const contentType = request.headers['content-type'] || '';
105
- let body = '';
106
-
107
- // Collect body chunks
108
- const chunks = [];
109
- let size = 0;
110
-
111
- return new Promise((resolve) => {
112
- request.on('data', (chunk) => {
113
- size += chunk.length;
114
- if (size <= maxSize) {
115
- chunks.push(chunk);
116
- }
117
- });
118
-
119
- request.on('end', () => {
120
- if (size > maxSize) {
121
- resolve({
122
- captured: false,
123
- reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
124
- size
125
- });
126
- return;
127
- }
128
-
129
- body = Buffer.concat(chunks).toString('utf8');
130
-
131
- // Parse based on content type
132
- if (contentType.includes('application/json')) {
133
- try {
134
- const parsed = JSON.parse(body);
135
- resolve({
136
- captured: true,
137
- type: 'json',
138
- body: parsed,
139
- size
140
- });
141
- } catch (e) {
142
- resolve({
143
- captured: true,
144
- type: 'json',
145
- body: body.substring(0, 1000),
146
- parseError: true,
147
- size
148
- });
149
- }
150
- } else if (contentType.includes('application/graphql')) {
151
- // GraphQL queries need redaction too!
152
- resolve({
153
- captured: true,
154
- type: 'graphql',
155
- body: body, // Will be redacted later
156
- size
157
- });
158
- } else if (contentType.includes('multipart/form-data')) {
159
- // Multipart is NOT captured (files can be huge)
160
- resolve({
161
- captured: false,
162
- type: 'multipart',
163
- reason: 'Multipart data not captured (file uploads)',
164
- size
165
- });
166
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
167
- try {
168
- const params = new URLSearchParams(body);
169
- const parsed = Object.fromEntries(params);
170
- resolve({
171
- captured: true,
172
- type: 'form',
173
- body: parsed,
174
- size
175
- });
176
- } catch (e) {
177
- resolve({
178
- captured: true,
179
- type: 'form',
180
- body: body.substring(0, 1000),
181
- size
182
- });
183
- }
184
- } else {
185
- resolve({
186
- captured: true,
187
- type: 'text',
188
- body: body.substring(0, 1000),
189
- size
190
- });
191
- }
192
- });
193
-
194
- request.on('error', () => {
195
- resolve({ captured: false, reason: 'Stream error' });
196
- });
197
-
198
- // Timeout after 100ms
199
- setTimeout(() => {
200
- resolve({ captured: false, reason: 'Timeout' });
201
- }, 100);
202
- });
203
- } catch (error) {
204
- return { captured: false, reason: error.message };
205
- }
206
- }
207
-
208
- /**
209
- * Register SecureNow OpenTelemetry for Next.js using @vercel/otel
210
- * @param {Object} options - Optional configuration
211
- * @param {string} options.serviceName - Service name (defaults to SECURENOW_APPID or OTEL_SERVICE_NAME)
212
- * @param {string} options.endpoint - Traces endpoint (defaults to SECURENOW_INSTANCE)
213
- * @param {boolean} options.noUuid - Don't append UUID to service name
214
- */
215
- function registerSecureNow(options = {}) {
216
- // Only register once
217
- if (isRegistered) {
218
- console.log('[securenow] Already registered, skipping...');
219
- return;
220
- }
221
-
222
- // Skip for Edge runtime
223
- if (process.env.NEXT_RUNTIME === 'edge') {
224
- console.log('[securenow] Skipping Edge runtime (Node.js only)');
225
- return;
226
- }
227
-
228
- // Detect environment outside try block for error handling
229
- const isVercel = !!(env('VERCEL') || env('VERCEL_ENV') || env('VERCEL_URL'));
230
-
231
- try {
232
- console.log('[securenow] Next.js integration loading (pid=%d)', process.pid);
233
-
234
- // -------- Configuration --------
235
- const rawBase = (
236
- options.serviceName ||
237
- env('OTEL_SERVICE_NAME') ||
238
- env('SECURENOW_APPID') ||
239
- ''
240
- ).trim().replace(/^['"]|['"]$/g, '');
241
-
242
- const baseName = rawBase || null;
243
- const noUuid = options.noUuid ?? (String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
244
-
245
- // service.name
246
- let serviceName;
247
- if (baseName) {
248
- serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
249
- } else {
250
- serviceName = `nextjs-app-${uuidv4()}`;
251
- console.warn('[securenow] โš ๏ธ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
252
- console.warn('[securenow] ๐Ÿ’ก Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
253
- }
254
-
255
- // -------- Endpoint Configuration --------
256
- const endpointBase = (
257
- options.endpoint ||
258
- env('SECURENOW_INSTANCE') ||
259
- env('OTEL_EXPORTER_OTLP_ENDPOINT') ||
260
- 'https://freetrial.securenow.ai:4318'
261
- ).replace(/\/$/, '');
262
-
263
- const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
264
- const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
265
-
266
- // Set environment variables for @vercel/otel to pick up
267
- process.env.OTEL_SERVICE_NAME = serviceName;
268
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
269
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
270
-
271
- // -------- Logging Configuration --------
272
- // Opt-in: SECURENOW_LOGGING_ENABLED=1 (or "true").
273
- const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) === '1' ||
274
- String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
275
-
276
- console.log('[securenow] ๐Ÿš€ Next.js App โ†’ service.name=%s', serviceName);
277
-
278
- // -------- Body Capture Configuration --------
279
- const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
280
- String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
281
- options.captureBody === true;
282
- const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
283
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
284
- const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
285
-
286
- // -------- Log environment detection --------
287
- if (!isVercel) {
288
- console.log('[securenow] ๐Ÿ–ฅ๏ธ Self-hosted environment detected (EC2/PM2) - using vanilla SDK');
289
- }
290
-
291
- // -------- Use different initialization based on environment --------
292
- const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
293
-
294
- // Configure HTTP instrumentation with comprehensive header capture
295
- const httpInstrumentation = new HttpInstrumentation({
296
- requireParentforOutgoingSpans: false,
297
- requireParentforIncomingSpans: false,
298
- ignoreIncomingRequestHook: (request) => {
299
- // Never ignore - we want to trace all requests
300
- return false;
301
- },
302
- requestHook: (span, request) => {
303
- // SYNCHRONOUS ONLY - no async operations to avoid timing issues
304
- try {
305
- // Capture all headers
306
- const headers = request.headers || {};
307
-
308
- // ======== IP ADDRESS CAPTURE ========
309
- // Try different header sources for IP (priority order)
310
- const forwardedFor = headers['x-forwarded-for'];
311
- const realIp = headers['x-real-ip'];
312
- const cfConnectingIp = headers['cf-connecting-ip']; // Cloudflare
313
- const clientIp = headers['x-client-ip'];
314
- const socketIp = request.socket?.remoteAddress;
315
-
316
- // Primary IP (first in chain is the real client)
317
- const primaryIp =
318
- (forwardedFor ? forwardedFor.split(',')[0]?.trim() : null) ||
319
- realIp ||
320
- cfConnectingIp ||
321
- clientIp ||
322
- socketIp ||
323
- 'unknown';
324
-
325
- // ======== PROTOCOL & CONNECTION ========
326
- const scheme = headers['x-forwarded-proto'] ||
327
- (request.socket?.encrypted ? 'https' : 'http');
328
- const host = headers['x-forwarded-host'] || headers['host'] || '';
329
- const port = headers['x-forwarded-port'] || request.socket?.localPort || '';
330
-
331
- // ======== REQUEST METADATA ========
332
- const userAgent = headers['user-agent'] || '';
333
- const referer = headers['referer'] || headers['referrer'] || '';
334
- const accept = headers['accept'] || '';
335
- const acceptLanguage = headers['accept-language'] || '';
336
- const acceptEncoding = headers['accept-encoding'] || '';
337
- const contentType = headers['content-type'] || '';
338
- const contentLength = headers['content-length'] || '';
339
- const origin = headers['origin'] || '';
340
-
341
- // ======== PROXY & LOAD BALANCER ========
342
- const originalUri = headers['x-original-uri'] || '';
343
- const originalMethod = headers['x-original-method'] || '';
344
- const requestId = headers['x-request-id'] || headers['x-trace-id'] || headers['x-correlation-id'] || '';
345
-
346
- // ======== SET ALL ATTRIBUTES ========
347
- const attributes = {
348
- // IP & Network
349
- 'http.client_ip': primaryIp,
350
- 'http.forwarded_for': forwardedFor || '',
351
- 'http.real_ip': realIp || '',
352
- 'http.socket_ip': socketIp || '',
353
-
354
- // Protocol & Host
355
- 'http.scheme': scheme,
356
- 'http.host': host,
357
- 'http.port': port.toString(),
358
- 'http.forwarded_proto': headers['x-forwarded-proto'] || '',
359
- 'http.forwarded_host': headers['x-forwarded-host'] || '',
360
-
361
- // Request Details
362
- 'http.user_agent': userAgent,
363
- 'http.referer': referer,
364
- 'http.origin': origin,
365
- 'http.accept': accept,
366
- 'http.accept_language': acceptLanguage,
367
- 'http.accept_encoding': acceptEncoding,
368
- 'http.content_type': contentType,
369
- 'http.content_length': contentLength,
370
-
371
- // Proxy & Routing
372
- 'http.original_uri': originalUri,
373
- 'http.original_method': originalMethod,
374
- 'http.request_id': requestId,
375
-
376
- // Connection Info
377
- 'http.connection': headers['connection'] || '',
378
- 'http.upgrade': headers['upgrade'] || '',
379
- };
380
-
381
- // Set all attributes at once
382
- span.setAttributes(attributes);
383
-
384
- // ======== GEOGRAPHIC DATA ========
385
- // Vercel geo headers
386
- if (headers['x-vercel-ip-country']) {
387
- span.setAttributes({
388
- 'http.geo.country': headers['x-vercel-ip-country'],
389
- 'http.geo.region': headers['x-vercel-ip-country-region'] || '',
390
- 'http.geo.city': headers['x-vercel-ip-city'] || '',
391
- 'http.geo.latitude': headers['x-vercel-ip-latitude'] || '',
392
- 'http.geo.longitude': headers['x-vercel-ip-longitude'] || '',
393
- 'http.geo.timezone': headers['x-vercel-ip-timezone'] || '',
394
- });
395
- }
396
-
397
- // Cloudflare geo headers
398
- if (headers['cf-ipcountry']) {
399
- span.setAttributes({
400
- 'http.geo.country': headers['cf-ipcountry'],
401
- 'http.geo.cf_ray': headers['cf-ray'] || '',
402
- 'http.geo.cf_visitor': headers['cf-visitor'] || '',
403
- });
404
- }
405
-
406
- // Cloudflare additional headers
407
- if (headers['cf-connecting-ip']) {
408
- span.setAttribute('http.cf.connecting_ip', headers['cf-connecting-ip']);
409
- }
410
-
411
- // ======== SECURITY HEADERS ========
412
- if (headers['x-csrf-token']) {
413
- span.setAttribute('http.security.csrf_token_present', 'true');
414
- }
415
- if (headers['authorization']) {
416
- span.setAttribute('http.security.auth_present', 'true');
417
- // Never log the actual token!
418
- }
419
- if (headers['cookie']) {
420
- span.setAttribute('http.security.cookies_present', 'true');
421
- // Never log actual cookies!
422
- }
423
-
424
- // Debug log in development
425
- if (env('NODE_ENV') === 'development' || env('OTEL_LOG_LEVEL') === 'debug') {
426
- console.log('[securenow] ๐Ÿ“ก Captured IP: %s (from: %s)',
427
- primaryIp,
428
- forwardedFor ? 'x-forwarded-for' : realIp ? 'x-real-ip' : socketIp ? 'socket' : 'unknown'
429
- );
430
- }
431
-
432
- // -------- Request Body NOT captured at HTTP instrumentation level --------
433
- // IMPORTANT: Do NOT attempt to read request.body or listen to 'data' events
434
- // Next.js manages request streams internally and reading them here causes conflicts
435
- // Body capture must be done in Next.js middleware using request.clone()
436
-
437
- } catch (error) {
438
- // Silently fail to not break the request
439
- if (env('OTEL_LOG_LEVEL') === 'debug') {
440
- console.error('[securenow] โš ๏ธ Error in requestHook:', error.message);
441
- }
442
- }
443
- },
444
- responseHook: (span, response) => {
445
- try {
446
- // Capture response metadata
447
- span.setAttributes({
448
- 'http.status_code': response.statusCode || 0,
449
- 'http.status_message': response.statusMessage || '',
450
- 'http.response.content_type': response.headers?.['content-type'] || '',
451
- 'http.response.content_length': response.headers?.['content-length'] || '',
452
- });
453
- } catch (error) {
454
- // Silently fail
455
- }
456
- },
457
- });
458
-
459
- // -------- Guard against OTLP exporter socket errors --------
460
- const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
461
- function _isOtlpTransientError(err) {
462
- if (!err) return false;
463
- if (_TRANSIENT_CODES.has(err.code)) return true;
464
- if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
465
- return false;
466
- }
467
- function _looksLikeOtlpStack(err) {
468
- const s = err && err.stack;
469
- if (!s) return false;
470
- return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
471
- || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
472
- }
473
- const _diagDebug = (env('OTEL_LOG_LEVEL') || '').toLowerCase() === 'debug';
474
- process.on('uncaughtException', (err, origin) => {
475
- if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
476
- if (_diagDebug) {
477
- console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
478
- }
479
- return;
480
- }
481
- throw err;
482
- });
483
- process.on('unhandledRejection', (reason) => {
484
- if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
485
- if (_diagDebug) {
486
- console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
487
- }
488
- return;
489
- }
490
- throw reason;
491
- });
492
-
493
- if (isVercel) {
494
- // -------- Vercel Environment: Use @vercel/otel --------
495
- const { registerOTel } = require('@vercel/otel');
496
-
497
- registerOTel({
498
- serviceName: serviceName,
499
- attributes: {
500
- 'deployment.environment': env('NODE_ENV') || env('VERCEL_ENV') || 'development',
501
- 'service.version': process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
502
- 'vercel.region': process.env.VERCEL_REGION || undefined,
503
- },
504
- instrumentations: [httpInstrumentation],
505
- instrumentationConfig: {
506
- fetch: {
507
- // Propagate context to your backend APIs
508
- propagateContextUrls: [
509
- /^https?:\/\/localhost/,
510
- /^https?:\/\/.*\.vercel\.app/,
511
- // Add your backend domains here
512
- ],
513
- // Optionally ignore certain URLs
514
- ignoreUrls: [
515
- /_next\/static/,
516
- /_next\/image/,
517
- /\.map$/,
518
- ],
519
- },
520
- },
521
- });
522
- } else {
523
- // -------- Self-Hosted (EC2/PM2): Use Vanilla OpenTelemetry SDK --------
524
- const { NodeSDK } = require('@opentelemetry/sdk-node');
525
- const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
526
- const { Resource } = require('@opentelemetry/resources');
527
- const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
528
-
529
- const traceExporter = new OTLPTraceExporter({
530
- url: tracesUrl,
531
- headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'))
532
- });
533
-
534
- const sdk = new NodeSDK({
535
- serviceName: serviceName,
536
- traceExporter: traceExporter,
537
- instrumentations: [httpInstrumentation],
538
- resource: new Resource({
539
- [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
540
- [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
541
- [SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
542
- }),
543
- });
544
-
545
- sdk.start();
546
- console.log('[securenow] ๐ŸŽฏ Vanilla SDK initialized for self-hosted environment');
547
- }
548
-
549
- // -------- Logging pipeline (both Vercel and self-hosted) --------
550
- // Neither @vercel/otel nor NodeSDK 0.47.x wires OTLP logs for us, so we
551
- // create the LoggerProvider ourselves, register a BatchLogRecordProcessor
552
- // (addLogRecordProcessor โ€” the `processors` constructor option was only
553
- // added in sdk-logs 0.52 and is silently ignored in 0.47), publish it as
554
- // the global logger provider, and auto-patch console.* to emit records.
555
- if (loggingEnabled) {
556
- const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
557
- const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
558
- const { logs } = require('@opentelemetry/api-logs');
559
- const { Resource } = require('@opentelemetry/resources');
560
- const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
561
-
562
- const logResource = new Resource({
563
- [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
564
- [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
565
- [SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
566
- });
567
-
568
- const logExporter = new OTLPLogExporter({
569
- url: logsUrl,
570
- headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS')),
571
- });
572
- const loggerProvider = new LoggerProvider({ resource: logResource });
573
- loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
574
- logs.setGlobalLoggerProvider(loggerProvider);
575
-
576
- const _logger = loggerProvider.getLogger('console', '1.0.0');
577
- const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
578
- const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
579
- const _emit = (sn, st, args) => {
580
- try {
581
- _logger.emit({
582
- severityNumber: sn,
583
- severityText: st,
584
- body: args.map(a => (typeof a === 'object' && a !== null)
585
- ? (() => { try { return JSON.stringify(a); } catch { return String(a); } })()
586
- : String(a)).join(' '),
587
- attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
588
- });
589
- } catch (_) {}
590
- };
591
- console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
592
- console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
593
- console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
594
- console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
595
- console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
596
-
597
- const _shutdownLogs = async () => {
598
- try { await Promise.resolve(loggerProvider.forceFlush?.()); } catch (_) {}
599
- try { await Promise.resolve(loggerProvider.shutdown?.()); } catch (_) {}
600
- };
601
- process.on('SIGINT', _shutdownLogs);
602
- process.on('SIGTERM', _shutdownLogs);
603
- process.on('beforeExit', _shutdownLogs);
604
-
605
- console.log('[securenow] ๐Ÿ“‹ Logging: ENABLED โ†’ %s', logsUrl);
606
- } else {
607
- console.log('[securenow] ๐Ÿ“‹ Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)');
608
- }
609
-
610
- isRegistered = true;
611
- console.log('[securenow] โœ… OpenTelemetry started for Next.js โ†’ %s', tracesUrl);
612
- console.log('[securenow] ๐Ÿ“Š Auto-capturing comprehensive request metadata:');
613
- console.log('[securenow] โ€ข IP addresses (x-forwarded-for, x-real-ip, socket)');
614
- console.log('[securenow] โ€ข User-Agent, Referer, Origin, Accept headers');
615
- console.log('[securenow] โ€ข Protocol, Host, Port (proxy-aware)');
616
- console.log('[securenow] โ€ข Geographic data (Vercel/Cloudflare)');
617
- console.log('[securenow] โ€ข Request IDs, CSRF tokens, Auth presence');
618
- console.log('[securenow] โ€ข Response status, content-type, content-length');
619
- console.log('[securenow] โš ๏ธ Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
620
- if (captureBody) {
621
- console.log('[securenow] ๐Ÿ’ก For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
622
- }
623
-
624
- // Optional test span
625
- if (String(env('SECURENOW_TEST_SPAN')) === '1') {
626
- const api = require('@opentelemetry/api');
627
- const tracer = api.trace.getTracer('securenow-nextjs');
628
- const span = tracer.startSpan('securenow.nextjs.startup');
629
- span.setAttribute('next.runtime', process.env.NEXT_RUNTIME || 'nodejs');
630
- span.end();
631
- console.log('[securenow] ๐Ÿงช Test span created');
632
- }
633
-
634
- } catch (error) {
635
- console.error('[securenow] Failed to initialize OpenTelemetry:', error);
636
- if (isVercel) {
637
- console.error('[securenow] Make sure you have @vercel/otel installed: npm install @vercel/otel');
638
- } else {
639
- console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
640
- }
641
- }
642
- }
643
-
644
- module.exports = {
645
- registerSecureNow,
646
- };
647
-
1
+ 'use strict';
2
+
3
+ /**
4
+ * SecureNow Next.js Integration using @vercel/otel
5
+ *
6
+ * Usage in Next.js app:
7
+ *
8
+ * 1. Add serverExternalPackages to next.config.js (REQUIRED to avoid webpack bundling issues):
9
+ *
10
+ * const nextConfig = {
11
+ * serverExternalPackages: [
12
+ * "securenow",
13
+ * "@opentelemetry/sdk-node",
14
+ * "@opentelemetry/auto-instrumentations-node",
15
+ * "@opentelemetry/instrumentation-http",
16
+ * "@opentelemetry/exporter-trace-otlp-http",
17
+ * "@opentelemetry/exporter-logs-otlp-http",
18
+ * "@opentelemetry/sdk-logs",
19
+ * "@opentelemetry/instrumentation",
20
+ * "@opentelemetry/resources",
21
+ * "@opentelemetry/semantic-conventions",
22
+ * "@opentelemetry/api",
23
+ * "@opentelemetry/api-logs",
24
+ * "@vercel/otel",
25
+ * ],
26
+ * };
27
+ *
28
+ * 2. Create instrumentation.ts (or .js) in your project root:
29
+ *
30
+ * import { registerSecureNow } from 'securenow/nextjs';
31
+ * export function register() {
32
+ * registerSecureNow();
33
+ * }
34
+ *
35
+ * 3. Set environment variables:
36
+ * SECURENOW_APPID=my-nextjs-app
37
+ * SECURENOW_INSTANCE=http://your-otlp-backend:4318
38
+ */
39
+
40
+ const { randomUUID } = require('crypto');
41
+
42
+ const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
43
+
44
+ const parseHeaders = str => {
45
+ const out = {}; if (!str) return out;
46
+ for (const raw of String(str).split(',')) {
47
+ const s = raw.trim(); if (!s) continue;
48
+ const i = s.indexOf('='); if (i === -1) continue;
49
+ out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
50
+ }
51
+ return out;
52
+ };
53
+
54
+ let isRegistered = false;
55
+
56
+ // Default sensitive fields to redact from request bodies
57
+ const DEFAULT_SENSITIVE_FIELDS = [
58
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
59
+ 'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
60
+ 'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
61
+ ];
62
+
63
+ /**
64
+ * Redact sensitive fields from an object
65
+ */
66
+ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
67
+ if (!obj || typeof obj !== 'object') return obj;
68
+
69
+ const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
70
+
71
+ for (const key of Object.keys(redacted)) {
72
+ const lowerKey = key.toLowerCase();
73
+
74
+ if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
75
+ redacted[key] = '[REDACTED]';
76
+ } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
77
+ // Recursively redact nested objects
78
+ redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
79
+ }
80
+ }
81
+
82
+ return redacted;
83
+ }
84
+
85
+ function escapeRegex(str) {
86
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
87
+ }
88
+
89
+ /**
90
+ * Redact sensitive data from GraphQL query strings
91
+ */
92
+ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
93
+ if (!query || typeof query !== 'string') return query;
94
+
95
+ let redacted = query;
96
+
97
+ // Redact sensitive fields in GraphQL arguments and variables
98
+ // Matches patterns like: password: "value" or password:"value" or password:'value'
99
+ sensitiveFields.forEach(field => {
100
+ const escaped = escapeRegex(field);
101
+ const patterns = [
102
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
103
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
104
+ ];
105
+
106
+ patterns.forEach(pattern => {
107
+ redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
108
+ if (suffix) {
109
+ return `${prefix}[REDACTED]${suffix}`;
110
+ } else {
111
+ return `${prefix}[REDACTED]`;
112
+ }
113
+ });
114
+ });
115
+ });
116
+
117
+ return redacted;
118
+ }
119
+
120
+ /**
121
+ * Register SecureNow OpenTelemetry for Next.js using @vercel/otel
122
+ * @param {Object} options - Optional configuration
123
+ * @param {string} options.serviceName - Service name (defaults to SECURENOW_APPID or OTEL_SERVICE_NAME)
124
+ * @param {string} options.endpoint - Traces endpoint (defaults to SECURENOW_INSTANCE)
125
+ * @param {boolean} options.noUuid - Don't append UUID to service name
126
+ */
127
+ function registerSecureNow(options = {}) {
128
+ // Only register once
129
+ if (isRegistered) {
130
+ console.log('[securenow] Already registered, skipping...');
131
+ return;
132
+ }
133
+
134
+ // Skip for Edge runtime
135
+ if (process.env.NEXT_RUNTIME === 'edge') {
136
+ console.log('[securenow] Skipping Edge runtime (Node.js only)');
137
+ return;
138
+ }
139
+
140
+ // Detect environment outside try block for error handling
141
+ const isVercel = !!(env('VERCEL') || env('VERCEL_ENV') || env('VERCEL_URL'));
142
+
143
+ try {
144
+ console.log('[securenow] Next.js integration loading (pid=%d)', process.pid);
145
+
146
+ // -------- Configuration --------
147
+ const rawBase = (
148
+ options.serviceName ||
149
+ env('OTEL_SERVICE_NAME') ||
150
+ env('SECURENOW_APPID') ||
151
+ ''
152
+ ).trim().replace(/^['"]|['"]$/g, '');
153
+
154
+ const baseName = rawBase || null;
155
+ const noUuid = options.noUuid ?? (String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
156
+
157
+ // service.name
158
+ let serviceName;
159
+ if (baseName) {
160
+ serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
161
+ } else {
162
+ serviceName = `nextjs-app-${randomUUID()}`;
163
+ console.warn('[securenow] โš ๏ธ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
164
+ console.warn('[securenow] ๐Ÿ’ก Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
165
+ }
166
+
167
+ // -------- Endpoint Configuration --------
168
+ const endpointBase = (
169
+ options.endpoint ||
170
+ env('SECURENOW_INSTANCE') ||
171
+ env('OTEL_EXPORTER_OTLP_ENDPOINT') ||
172
+ 'https://freetrial.securenow.ai:4318'
173
+ ).replace(/\/$/, '');
174
+
175
+ const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
176
+ const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
177
+
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;
181
+
182
+ console.log('[securenow] ๐Ÿš€ Next.js App โ†’ service.name=%s', serviceName);
183
+
184
+ // -------- Body Capture Configuration --------
185
+ const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
186
+ String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
187
+ options.captureBody === true;
188
+ const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
189
+ const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
190
+ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
191
+
192
+ // -------- Log environment detection --------
193
+ if (!isVercel) {
194
+ console.log('[securenow] ๐Ÿ–ฅ๏ธ Self-hosted environment detected (EC2/PM2) - using vanilla SDK');
195
+ }
196
+
197
+ // -------- Use different initialization based on environment --------
198
+ const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
199
+
200
+ // Configure HTTP instrumentation with comprehensive header capture
201
+ const httpInstrumentation = new HttpInstrumentation({
202
+ requireParentforOutgoingSpans: false,
203
+ requireParentforIncomingSpans: false,
204
+ ignoreIncomingRequestHook: (request) => {
205
+ // Never ignore - we want to trace all requests
206
+ return false;
207
+ },
208
+ requestHook: (span, request) => {
209
+ // SYNCHRONOUS ONLY - no async operations to avoid timing issues
210
+ try {
211
+ // Capture all headers
212
+ const headers = request.headers || {};
213
+
214
+ // ======== IP ADDRESS CAPTURE ========
215
+ // Try different header sources for IP (priority order)
216
+ const forwardedFor = headers['x-forwarded-for'];
217
+ const realIp = headers['x-real-ip'];
218
+ const cfConnectingIp = headers['cf-connecting-ip']; // Cloudflare
219
+ const clientIp = headers['x-client-ip'];
220
+ const socketIp = request.socket?.remoteAddress;
221
+
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
+ }
235
+
236
+ // ======== PROTOCOL & CONNECTION ========
237
+ const scheme = headers['x-forwarded-proto'] ||
238
+ (request.socket?.encrypted ? 'https' : 'http');
239
+ const host = headers['x-forwarded-host'] || headers['host'] || '';
240
+ const port = headers['x-forwarded-port'] || request.socket?.localPort || '';
241
+
242
+ // ======== REQUEST METADATA ========
243
+ const userAgent = headers['user-agent'] || '';
244
+ const referer = headers['referer'] || headers['referrer'] || '';
245
+ const accept = headers['accept'] || '';
246
+ const acceptLanguage = headers['accept-language'] || '';
247
+ const acceptEncoding = headers['accept-encoding'] || '';
248
+ const contentType = headers['content-type'] || '';
249
+ const contentLength = headers['content-length'] || '';
250
+ const origin = headers['origin'] || '';
251
+
252
+ // ======== PROXY & LOAD BALANCER ========
253
+ const originalUri = headers['x-original-uri'] || '';
254
+ const originalMethod = headers['x-original-method'] || '';
255
+ const requestId = headers['x-request-id'] || headers['x-trace-id'] || headers['x-correlation-id'] || '';
256
+
257
+ // ======== SET ALL ATTRIBUTES ========
258
+ const attributes = {
259
+ // IP & Network
260
+ 'http.client_ip': primaryIp,
261
+ 'http.forwarded_for': forwardedFor || '',
262
+ 'http.real_ip': realIp || '',
263
+ 'http.socket_ip': socketIp || '',
264
+
265
+ // Protocol & Host
266
+ 'http.scheme': scheme,
267
+ 'http.host': host,
268
+ 'http.port': port.toString(),
269
+ 'http.forwarded_proto': headers['x-forwarded-proto'] || '',
270
+ 'http.forwarded_host': headers['x-forwarded-host'] || '',
271
+
272
+ // Request Details
273
+ 'http.user_agent': userAgent,
274
+ 'http.referer': referer,
275
+ 'http.origin': origin,
276
+ 'http.accept': accept,
277
+ 'http.accept_language': acceptLanguage,
278
+ 'http.accept_encoding': acceptEncoding,
279
+ 'http.content_type': contentType,
280
+ 'http.content_length': contentLength,
281
+
282
+ // Proxy & Routing
283
+ 'http.original_uri': originalUri,
284
+ 'http.original_method': originalMethod,
285
+ 'http.request_id': requestId,
286
+
287
+ // Connection Info
288
+ 'http.connection': headers['connection'] || '',
289
+ 'http.upgrade': headers['upgrade'] || '',
290
+ };
291
+
292
+ // Set all attributes at once
293
+ span.setAttributes(attributes);
294
+
295
+ // ======== GEOGRAPHIC DATA ========
296
+ // Vercel geo headers
297
+ if (headers['x-vercel-ip-country']) {
298
+ span.setAttributes({
299
+ 'http.geo.country': headers['x-vercel-ip-country'],
300
+ 'http.geo.region': headers['x-vercel-ip-country-region'] || '',
301
+ 'http.geo.city': headers['x-vercel-ip-city'] || '',
302
+ 'http.geo.latitude': headers['x-vercel-ip-latitude'] || '',
303
+ 'http.geo.longitude': headers['x-vercel-ip-longitude'] || '',
304
+ 'http.geo.timezone': headers['x-vercel-ip-timezone'] || '',
305
+ });
306
+ }
307
+
308
+ // Cloudflare geo headers
309
+ if (headers['cf-ipcountry']) {
310
+ span.setAttributes({
311
+ 'http.geo.country': headers['cf-ipcountry'],
312
+ 'http.geo.cf_ray': headers['cf-ray'] || '',
313
+ 'http.geo.cf_visitor': headers['cf-visitor'] || '',
314
+ });
315
+ }
316
+
317
+ // Cloudflare additional headers
318
+ if (headers['cf-connecting-ip']) {
319
+ span.setAttribute('http.cf.connecting_ip', headers['cf-connecting-ip']);
320
+ }
321
+
322
+ // ======== SECURITY HEADERS ========
323
+ if (headers['x-csrf-token']) {
324
+ span.setAttribute('http.security.csrf_token_present', 'true');
325
+ }
326
+ if (headers['authorization']) {
327
+ span.setAttribute('http.security.auth_present', 'true');
328
+ // Never log the actual token!
329
+ }
330
+ if (headers['cookie']) {
331
+ span.setAttribute('http.security.cookies_present', 'true');
332
+ // Never log actual cookies!
333
+ }
334
+
335
+ // Debug log in development
336
+ if (env('NODE_ENV') === 'development' || env('OTEL_LOG_LEVEL') === 'debug') {
337
+ console.log('[securenow] ๐Ÿ“ก Captured IP: %s (from: %s)',
338
+ primaryIp,
339
+ forwardedFor ? 'x-forwarded-for' : realIp ? 'x-real-ip' : socketIp ? 'socket' : 'unknown'
340
+ );
341
+ }
342
+
343
+ // -------- Request Body NOT captured at HTTP instrumentation level --------
344
+ // IMPORTANT: Do NOT attempt to read request.body or listen to 'data' events
345
+ // Next.js manages request streams internally and reading them here causes conflicts
346
+ // Body capture must be done in Next.js middleware using request.clone()
347
+
348
+ } catch (error) {
349
+ // Silently fail to not break the request
350
+ if (env('OTEL_LOG_LEVEL') === 'debug') {
351
+ console.error('[securenow] โš ๏ธ Error in requestHook:', error.message);
352
+ }
353
+ }
354
+ },
355
+ responseHook: (span, response) => {
356
+ try {
357
+ // Capture response metadata
358
+ span.setAttributes({
359
+ 'http.status_code': response.statusCode || 0,
360
+ 'http.status_message': response.statusMessage || '',
361
+ 'http.response.content_type': response.headers?.['content-type'] || '',
362
+ 'http.response.content_length': response.headers?.['content-length'] || '',
363
+ });
364
+ } catch (error) {
365
+ // Silently fail
366
+ }
367
+ },
368
+ });
369
+
370
+ // -------- Guard against OTLP exporter socket errors --------
371
+ // The OTLP HTTP exporter uses keep-alive connections that can be reset by
372
+ // the remote end (ECONNRESET / "socket hang up"). These transient errors
373
+ // sometimes escape as unhandled exceptions or rejections. We catch them
374
+ // here and log at debug level instead of crashing the host app.
375
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
376
+ function _isOtlpTransientError(err) {
377
+ if (!err) return false;
378
+ if (_TRANSIENT_CODES.has(err.code)) return true;
379
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
380
+ return false;
381
+ }
382
+ function _looksLikeOtlpStack(err) {
383
+ const s = err && err.stack;
384
+ if (!s) return false;
385
+ return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
386
+ || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
387
+ }
388
+ const _diagDebug = (env('OTEL_LOG_LEVEL') || '').toLowerCase() === 'debug';
389
+ process.on('uncaughtException', (err, origin) => {
390
+ if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
391
+ if (_diagDebug) {
392
+ console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
393
+ }
394
+ return;
395
+ }
396
+ throw err;
397
+ });
398
+ process.on('unhandledRejection', (reason) => {
399
+ if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
400
+ if (_diagDebug) {
401
+ console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
402
+ }
403
+ return;
404
+ }
405
+ throw reason;
406
+ });
407
+
408
+ if (isVercel) {
409
+ // -------- Vercel Environment: Use @vercel/otel --------
410
+ const { registerOTel } = require('@vercel/otel');
411
+
412
+ registerOTel({
413
+ serviceName: serviceName,
414
+ attributes: {
415
+ 'deployment.environment': env('NODE_ENV') || env('VERCEL_ENV') || 'development',
416
+ 'service.version': process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
417
+ 'vercel.region': process.env.VERCEL_REGION || undefined,
418
+ },
419
+ instrumentations: [httpInstrumentation],
420
+ instrumentationConfig: {
421
+ fetch: {
422
+ // Propagate context to your backend APIs
423
+ propagateContextUrls: [
424
+ /^https?:\/\/localhost/,
425
+ /^https?:\/\/.*\.vercel\.app/,
426
+ // Add your backend domains here
427
+ ],
428
+ // Optionally ignore certain URLs
429
+ ignoreUrls: [
430
+ /_next\/static/,
431
+ /_next\/image/,
432
+ /\.map$/,
433
+ ],
434
+ },
435
+ },
436
+ });
437
+ } else {
438
+ // -------- Self-Hosted (EC2/PM2): Use Vanilla OpenTelemetry SDK --------
439
+ const { NodeSDK } = require('@opentelemetry/sdk-node');
440
+ const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
441
+ const { Resource } = require('@opentelemetry/resources');
442
+ const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
443
+
444
+ const traceExporter = new OTLPTraceExporter({
445
+ url: tracesUrl,
446
+ headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'))
447
+ });
448
+
449
+ const sdk = new NodeSDK({
450
+ serviceName: serviceName,
451
+ traceExporter: traceExporter,
452
+ instrumentations: [httpInstrumentation],
453
+ resource: new Resource({
454
+ [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
455
+ [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
456
+ [SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
457
+ }),
458
+ });
459
+
460
+ sdk.start();
461
+ console.log('[securenow] ๐ŸŽฏ Vanilla SDK initialized for self-hosted environment');
462
+
463
+ // -------- Logging (self-hosted only) --------
464
+ const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) === '1' || String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
465
+ if (loggingEnabled) {
466
+ try {
467
+ const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
468
+ const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
469
+
470
+ const logExporter = new OTLPLogExporter({
471
+ url: logsUrl,
472
+ headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS')),
473
+ });
474
+
475
+ const loggerProvider = new LoggerProvider({
476
+ resource: new Resource({
477
+ [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
478
+ [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || 'production',
479
+ }),
480
+ });
481
+ loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
482
+
483
+ // Patch console to forward logs as OTLP log records
484
+ const logger = loggerProvider.getLogger('console', '1.0.0');
485
+ const SeverityNumber = { INFO: 9, WARN: 13, ERROR: 17 };
486
+ const origLog = console.log;
487
+ const origWarn = console.warn;
488
+ const origError = console.error;
489
+
490
+ const { context: otelContext, trace: otelTrace } = require('@opentelemetry/api');
491
+ function _emitLog(sn, st, args) {
492
+ try {
493
+ const activeCtx = otelContext.active();
494
+ const spanCtx = otelTrace.getSpanContext(activeCtx);
495
+ logger.emit({
496
+ severityNumber: sn,
497
+ severityText: st,
498
+ body: args.map(String).join(' '),
499
+ ...(spanCtx && { context: activeCtx }),
500
+ });
501
+ } catch (_) {}
502
+ }
503
+ console.log = (...args) => {
504
+ origLog.apply(console, args);
505
+ _emitLog(SeverityNumber.INFO, 'INFO', args);
506
+ };
507
+ console.warn = (...args) => {
508
+ origWarn.apply(console, args);
509
+ _emitLog(SeverityNumber.WARN, 'WARN', args);
510
+ };
511
+ console.error = (...args) => {
512
+ origError.apply(console, args);
513
+ _emitLog(SeverityNumber.ERROR, 'ERROR', args);
514
+ };
515
+
516
+ console.log('[securenow] ๐Ÿ“‹ Logging: ENABLED โ†’ %s', logsUrl);
517
+
518
+ // Auto-log every incoming HTTP request/response
519
+ try {
520
+ const http = require('http');
521
+ const originalEmit = http.Server.prototype.emit;
522
+ http.Server.prototype.emit = function (event, req, res) {
523
+ if (event === 'request' && req && res) {
524
+ const start = Date.now();
525
+ const method = req.method;
526
+ const url = req.url;
527
+ res.on('finish', () => {
528
+ const reqCtx = otelContext.active();
529
+ const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
530
+ const duration = Date.now() - start;
531
+ const status = res.statusCode;
532
+ const ip = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.socket?.remoteAddress || '-';
533
+ const ua = req.headers['user-agent'] || '-';
534
+ const body = `${method} ${url} ${status} ${duration}ms ip=${ip} ua=${ua}`;
535
+ const severity = status >= 500 ? SeverityNumber.ERROR : status >= 400 ? SeverityNumber.WARN : SeverityNumber.INFO;
536
+ const severityText = status >= 500 ? 'ERROR' : status >= 400 ? 'WARN' : 'INFO';
537
+ origLog.call(console, '[securenow] %s %s %d %dms', method, url, status, duration);
538
+ try {
539
+ logger.emit({
540
+ severityNumber: severity,
541
+ severityText,
542
+ body,
543
+ attributes: {
544
+ 'http.method': method,
545
+ 'http.url': url,
546
+ 'http.status_code': status,
547
+ 'http.duration_ms': duration,
548
+ 'http.client_ip': String(ip).split(',')[0].trim(),
549
+ 'http.user_agent': ua,
550
+ },
551
+ ...(reqSpanCtx && { context: reqCtx }),
552
+ });
553
+ } catch (_) {}
554
+ });
555
+ }
556
+ return originalEmit.apply(this, arguments);
557
+ };
558
+ console.log('[securenow] ๐Ÿ“‹ HTTP request logging: ENABLED');
559
+ } catch (_) {}
560
+
561
+ // Graceful shutdown for logs
562
+ process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
563
+ process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
564
+ } catch (e) {
565
+ console.warn('[securenow] โš ๏ธ Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
566
+ }
567
+ } else {
568
+ console.log('[securenow] ๐Ÿ“‹ Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)');
569
+ }
570
+ }
571
+
572
+ isRegistered = true;
573
+
574
+ // Free trial banner (optional โ€” may not be bundled in standalone builds)
575
+ try {
576
+ const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
577
+ if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
578
+ patchHttpForBanner();
579
+ }
580
+ } catch (_) {}
581
+
582
+ console.log('[securenow] โœ… OpenTelemetry started for Next.js โ†’ %s', tracesUrl);
583
+ console.log('[securenow] ๐Ÿ“Š Auto-capturing comprehensive request metadata:');
584
+ console.log('[securenow] โ€ข IP addresses (x-forwarded-for, x-real-ip, socket)');
585
+ console.log('[securenow] โ€ข User-Agent, Referer, Origin, Accept headers');
586
+ console.log('[securenow] โ€ข Protocol, Host, Port (proxy-aware)');
587
+ console.log('[securenow] โ€ข Geographic data (Vercel/Cloudflare)');
588
+ console.log('[securenow] โ€ข Request IDs, CSRF tokens, Auth presence');
589
+ console.log('[securenow] โ€ข Response status, content-type, content-length');
590
+ console.log('[securenow] โš ๏ธ Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
591
+ if (captureBody) {
592
+ console.log('[securenow] ๐Ÿ’ก For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
593
+ }
594
+
595
+ // Optional test span
596
+ if (String(env('SECURENOW_TEST_SPAN')) === '1') {
597
+ const api = require('@opentelemetry/api');
598
+ const tracer = api.trace.getTracer('securenow-nextjs');
599
+ const span = tracer.startSpan('securenow.nextjs.startup');
600
+ span.setAttribute('next.runtime', process.env.NEXT_RUNTIME || 'nodejs');
601
+ span.end();
602
+ console.log('[securenow] ๐Ÿงช Test span created');
603
+ }
604
+
605
+ } catch (error) {
606
+ console.error('[securenow] Failed to initialize OpenTelemetry:', error);
607
+ if (isVercel) {
608
+ console.error('[securenow] Make sure you have @vercel/otel installed: npm install @vercel/otel');
609
+ } else {
610
+ console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
611
+ }
612
+ }
613
+
614
+ // Firewall โ€” runs independently from OTel so it works even if tracing fails
615
+ const firewallApiKey = env('SECURENOW_API_KEY');
616
+ if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
617
+ try {
618
+ require('./firewall').init({
619
+ apiKey: firewallApiKey,
620
+ apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
621
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
622
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
623
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
624
+ statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
625
+ log: env('SECURENOW_FIREWALL_LOG') !== '0',
626
+ tcp: env('SECURENOW_FIREWALL_TCP') === '1',
627
+ iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
628
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
629
+ });
630
+ } catch (e) {
631
+ console.warn('[securenow] Firewall init failed:', e.message);
632
+ }
633
+ }
634
+ }
635
+
636
+ module.exports = {
637
+ registerSecureNow,
638
+ };
639
+