securenow 6.0.1 โ†’ 6.0.2

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 (3) hide show
  1. package/nextjs.js +647 -613
  2. package/package.json +164 -164
  3. package/tracing.js +42 -1
package/nextjs.js CHANGED
@@ -1,613 +1,647 @@
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
- if (isVercel) {
460
- // -------- Vercel Environment: Use @vercel/otel --------
461
- const { registerOTel } = require('@vercel/otel');
462
-
463
- registerOTel({
464
- serviceName: serviceName,
465
- attributes: {
466
- 'deployment.environment': env('NODE_ENV') || env('VERCEL_ENV') || 'development',
467
- 'service.version': process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
468
- 'vercel.region': process.env.VERCEL_REGION || undefined,
469
- },
470
- instrumentations: [httpInstrumentation],
471
- instrumentationConfig: {
472
- fetch: {
473
- // Propagate context to your backend APIs
474
- propagateContextUrls: [
475
- /^https?:\/\/localhost/,
476
- /^https?:\/\/.*\.vercel\.app/,
477
- // Add your backend domains here
478
- ],
479
- // Optionally ignore certain URLs
480
- ignoreUrls: [
481
- /_next\/static/,
482
- /_next\/image/,
483
- /\.map$/,
484
- ],
485
- },
486
- },
487
- });
488
- } else {
489
- // -------- Self-Hosted (EC2/PM2): Use Vanilla OpenTelemetry SDK --------
490
- const { NodeSDK } = require('@opentelemetry/sdk-node');
491
- const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
492
- const { Resource } = require('@opentelemetry/resources');
493
- const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
494
-
495
- const traceExporter = new OTLPTraceExporter({
496
- url: tracesUrl,
497
- headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'))
498
- });
499
-
500
- const sdk = new NodeSDK({
501
- serviceName: serviceName,
502
- traceExporter: traceExporter,
503
- instrumentations: [httpInstrumentation],
504
- resource: new Resource({
505
- [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
506
- [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
507
- [SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
508
- }),
509
- });
510
-
511
- sdk.start();
512
- console.log('[securenow] ๐ŸŽฏ Vanilla SDK initialized for self-hosted environment');
513
- }
514
-
515
- // -------- Logging pipeline (both Vercel and self-hosted) --------
516
- // Neither @vercel/otel nor NodeSDK 0.47.x wires OTLP logs for us, so we
517
- // create the LoggerProvider ourselves, register a BatchLogRecordProcessor
518
- // (addLogRecordProcessor โ€” the `processors` constructor option was only
519
- // added in sdk-logs 0.52 and is silently ignored in 0.47), publish it as
520
- // the global logger provider, and auto-patch console.* to emit records.
521
- if (loggingEnabled) {
522
- const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
523
- const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
524
- const { logs } = require('@opentelemetry/api-logs');
525
- const { Resource } = require('@opentelemetry/resources');
526
- const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
527
-
528
- const logResource = new Resource({
529
- [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
530
- [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
531
- [SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
532
- });
533
-
534
- const logExporter = new OTLPLogExporter({
535
- url: logsUrl,
536
- headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS')),
537
- });
538
- const loggerProvider = new LoggerProvider({ resource: logResource });
539
- loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
540
- logs.setGlobalLoggerProvider(loggerProvider);
541
-
542
- const _logger = loggerProvider.getLogger('console', '1.0.0');
543
- const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
544
- const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
545
- const _emit = (sn, st, args) => {
546
- try {
547
- _logger.emit({
548
- severityNumber: sn,
549
- severityText: st,
550
- body: args.map(a => (typeof a === 'object' && a !== null)
551
- ? (() => { try { return JSON.stringify(a); } catch { return String(a); } })()
552
- : String(a)).join(' '),
553
- attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
554
- });
555
- } catch (_) {}
556
- };
557
- console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
558
- console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
559
- console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
560
- console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
561
- console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
562
-
563
- const _shutdownLogs = async () => {
564
- try { await Promise.resolve(loggerProvider.forceFlush?.()); } catch (_) {}
565
- try { await Promise.resolve(loggerProvider.shutdown?.()); } catch (_) {}
566
- };
567
- process.on('SIGINT', _shutdownLogs);
568
- process.on('SIGTERM', _shutdownLogs);
569
- process.on('beforeExit', _shutdownLogs);
570
-
571
- console.log('[securenow] ๐Ÿ“‹ Logging: ENABLED โ†’ %s', logsUrl);
572
- } else {
573
- console.log('[securenow] ๐Ÿ“‹ Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)');
574
- }
575
-
576
- isRegistered = true;
577
- console.log('[securenow] โœ… OpenTelemetry started for Next.js โ†’ %s', tracesUrl);
578
- console.log('[securenow] ๐Ÿ“Š Auto-capturing comprehensive request metadata:');
579
- console.log('[securenow] โ€ข IP addresses (x-forwarded-for, x-real-ip, socket)');
580
- console.log('[securenow] โ€ข User-Agent, Referer, Origin, Accept headers');
581
- console.log('[securenow] โ€ข Protocol, Host, Port (proxy-aware)');
582
- console.log('[securenow] โ€ข Geographic data (Vercel/Cloudflare)');
583
- console.log('[securenow] โ€ข Request IDs, CSRF tokens, Auth presence');
584
- console.log('[securenow] โ€ข Response status, content-type, content-length');
585
- console.log('[securenow] โš ๏ธ Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
586
- if (captureBody) {
587
- console.log('[securenow] ๐Ÿ’ก For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
588
- }
589
-
590
- // Optional test span
591
- if (String(env('SECURENOW_TEST_SPAN')) === '1') {
592
- const api = require('@opentelemetry/api');
593
- const tracer = api.trace.getTracer('securenow-nextjs');
594
- const span = tracer.startSpan('securenow.nextjs.startup');
595
- span.setAttribute('next.runtime', process.env.NEXT_RUNTIME || 'nodejs');
596
- span.end();
597
- console.log('[securenow] ๐Ÿงช Test span created');
598
- }
599
-
600
- } catch (error) {
601
- console.error('[securenow] Failed to initialize OpenTelemetry:', error);
602
- if (isVercel) {
603
- console.error('[securenow] Make sure you have @vercel/otel installed: npm install @vercel/otel');
604
- } else {
605
- console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
606
- }
607
- }
608
- }
609
-
610
- module.exports = {
611
- registerSecureNow,
612
- };
613
-
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
+