securenow 6.0.1 → 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 +224 -198
  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 +67 -45
  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 +333 -31
  86. package/web-vite.mjs +239 -156
  87. package/LICENSE +0 -15
package/nextjs.js CHANGED
@@ -5,21 +5,39 @@
5
5
  *
6
6
  * Usage in Next.js app:
7
7
  *
8
- * 1. Create instrumentation.ts (or .js) in your project root:
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:
9
29
  *
10
30
  * import { registerSecureNow } from 'securenow/nextjs';
11
31
  * export function register() {
12
32
  * registerSecureNow();
13
33
  * }
14
34
  *
15
- * 2. Set environment variables:
35
+ * 3. Set environment variables:
16
36
  * SECURENOW_APPID=my-nextjs-app
17
- * SECURENOW_INSTANCE=http://your-signoz-host:4318
18
- *
19
- * That's it! 🎉 No webpack warnings!
37
+ * SECURENOW_INSTANCE=http://your-otlp-backend:4318
20
38
  */
21
39
 
22
- const { v4: uuidv4 } = require('uuid');
40
+ const { randomUUID } = require('crypto');
23
41
 
24
42
  const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
25
43
 
@@ -50,10 +68,9 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
50
68
 
51
69
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
52
70
 
53
- for (const key in redacted) {
71
+ for (const key of Object.keys(redacted)) {
54
72
  const lowerKey = key.toLowerCase();
55
73
 
56
- // Check if field is sensitive
57
74
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
58
75
  redacted[key] = '[REDACTED]';
59
76
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
@@ -65,6 +82,10 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
65
82
  return redacted;
66
83
  }
67
84
 
85
+ function escapeRegex(str) {
86
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
87
+ }
88
+
68
89
  /**
69
90
  * Redact sensitive data from GraphQL query strings
70
91
  */
@@ -76,10 +97,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
76
97
  // Redact sensitive fields in GraphQL arguments and variables
77
98
  // Matches patterns like: password: "value" or password:"value" or password:'value'
78
99
  sensitiveFields.forEach(field => {
79
- // Match field: "value" or field: 'value' or field:"value" (with optional spaces)
100
+ const escaped = escapeRegex(field);
80
101
  const patterns = [
81
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
82
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
102
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
103
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
83
104
  ];
84
105
 
85
106
  patterns.forEach(pattern => {
@@ -96,115 +117,6 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
96
117
  return redacted;
97
118
  }
98
119
 
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
120
  /**
209
121
  * Register SecureNow OpenTelemetry for Next.js using @vercel/otel
210
122
  * @param {Object} options - Optional configuration
@@ -245,9 +157,9 @@ function registerSecureNow(options = {}) {
245
157
  // service.name
246
158
  let serviceName;
247
159
  if (baseName) {
248
- serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
160
+ serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
249
161
  } else {
250
- serviceName = `nextjs-app-${uuidv4()}`;
162
+ serviceName = `nextjs-app-${randomUUID()}`;
251
163
  console.warn('[securenow] ⚠️ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
252
164
  console.warn('[securenow] 💡 Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
253
165
  }
@@ -261,17 +173,11 @@ function registerSecureNow(options = {}) {
261
173
  ).replace(/\/$/, '');
262
174
 
263
175
  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;
176
+ const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
270
177
 
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';
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;
275
181
 
276
182
  console.log('[securenow] 🚀 Next.js App → service.name=%s', serviceName);
277
183
 
@@ -279,7 +185,7 @@ function registerSecureNow(options = {}) {
279
185
  const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
280
186
  String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
281
187
  options.captureBody === true;
282
- const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
188
+ const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
283
189
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
284
190
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
285
191
 
@@ -313,14 +219,19 @@ function registerSecureNow(options = {}) {
313
219
  const clientIp = headers['x-client-ip'];
314
220
  const socketIp = request.socket?.remoteAddress;
315
221
 
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';
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
+ }
324
235
 
325
236
  // ======== PROTOCOL & CONNECTION ========
326
237
  const scheme = headers['x-forwarded-proto'] ||
@@ -456,6 +367,44 @@ function registerSecureNow(options = {}) {
456
367
  },
457
368
  });
458
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
+
459
408
  if (isVercel) {
460
409
  // -------- Vercel Environment: Use @vercel/otel --------
461
410
  const { registerOTel } = require('@vercel/otel');
@@ -510,70 +459,126 @@ function registerSecureNow(options = {}) {
510
459
 
511
460
  sdk.start();
512
461
  console.log('[securenow] 🎯 Vanilla SDK initialized for self-hosted environment');
513
- }
514
462
 
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');
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');
527
469
 
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
- });
470
+ const logExporter = new OTLPLogExporter({
471
+ url: logsUrl,
472
+ headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS')),
473
+ });
533
474
 
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() },
475
+ const loggerProvider = new LoggerProvider({
476
+ resource: new Resource({
477
+ [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
478
+ [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || 'production',
479
+ }),
554
480
  });
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)');
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
+ }
574
570
  }
575
571
 
576
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
+
577
582
  console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
578
583
  console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
579
584
  console.log('[securenow] • IP addresses (x-forwarded-for, x-real-ip, socket)');
@@ -605,6 +610,27 @@ function registerSecureNow(options = {}) {
605
610
  console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
606
611
  }
607
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
+ }
608
634
  }
609
635
 
610
636
  module.exports = {