securenow 6.0.2 → 7.0.0-anas

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 (88) 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/app-config.js +130 -0
  7. package/cidr.js +83 -0
  8. package/cli/apps.js +608 -0
  9. package/cli/auth.js +298 -0
  10. package/cli/client.js +115 -0
  11. package/cli/config.js +202 -0
  12. package/cli/diagnostics.js +387 -0
  13. package/cli/firewall.js +100 -0
  14. package/cli/fp.js +638 -0
  15. package/cli/init.js +201 -0
  16. package/cli/monitor.js +440 -0
  17. package/cli/run.js +148 -0
  18. package/cli/security.js +980 -0
  19. package/cli/ui.js +386 -0
  20. package/cli/utils.js +127 -0
  21. package/cli.js +469 -455
  22. package/console-instrumentation.js +147 -136
  23. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
  24. package/docs/API-KEYS-GUIDE.md +233 -0
  25. package/docs/ARCHITECTURE.md +3 -3
  26. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  27. package/docs/AUTO-SETUP-SUMMARY.md +331 -0
  28. package/docs/AUTO-SETUP.md +4 -4
  29. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  30. package/docs/BODY-CAPTURE-FIX.md +261 -0
  31. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  32. package/docs/CHANGELOG-NEXTJS.md +1 -35
  33. package/docs/COMPLETION-REPORT.md +408 -0
  34. package/docs/CUSTOMER-GUIDE.md +16 -16
  35. package/docs/EASIEST-SETUP.md +5 -5
  36. package/docs/ENVIRONMENT-VARIABLES.md +880 -652
  37. package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
  38. package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
  39. package/docs/FINAL-SOLUTION.md +335 -0
  40. package/docs/FIREWALL-GUIDE.md +426 -0
  41. package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
  42. package/docs/INDEX.md +22 -4
  43. package/docs/LOGGING-GUIDE.md +701 -708
  44. package/docs/LOGGING-QUICKSTART.md +234 -255
  45. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
  46. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  47. package/docs/NEXTJS-GUIDE.md +14 -14
  48. package/docs/NEXTJS-QUICKSTART.md +1 -1
  49. package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
  50. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  51. package/docs/NUXT-GUIDE.md +166 -0
  52. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  53. package/docs/REDACTION-EXAMPLES.md +1 -1
  54. package/docs/REQUEST-BODY-CAPTURE.md +19 -10
  55. package/docs/SOLUTION-SUMMARY.md +312 -0
  56. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  57. package/examples/README.md +6 -6
  58. package/examples/instrumentation-with-auto-capture.ts +1 -1
  59. package/examples/nextjs-env-example.txt +2 -2
  60. package/examples/nextjs-instrumentation.js +1 -1
  61. package/examples/nextjs-instrumentation.ts +1 -1
  62. package/examples/nextjs-with-logging-example.md +6 -6
  63. package/examples/nextjs-with-options.ts +1 -1
  64. package/examples/test-nextjs-setup.js +1 -1
  65. package/firewall-cloud.js +212 -0
  66. package/firewall-iptables.js +139 -0
  67. package/firewall-only.js +38 -0
  68. package/firewall-tcp.js +74 -0
  69. package/firewall.js +720 -0
  70. package/free-trial-banner.js +174 -0
  71. package/nextjs-auto-capture.js +198 -207
  72. package/nextjs-middleware.js +186 -181
  73. package/nextjs-webpack-config.js +88 -53
  74. package/nextjs-wrapper.js +158 -158
  75. package/nextjs.d.ts +1 -1
  76. package/nextjs.js +638 -647
  77. package/nuxt-server-plugin.mjs +425 -0
  78. package/nuxt.d.ts +60 -0
  79. package/nuxt.mjs +75 -0
  80. package/package.json +172 -164
  81. package/postinstall.js +42 -14
  82. package/register.d.ts +1 -1
  83. package/register.js +39 -4
  84. package/resolve-ip.js +77 -0
  85. package/tracing.d.ts +2 -1
  86. package/tracing.js +318 -45
  87. package/web-vite.mjs +239 -156
  88. package/LICENSE +0 -15
package/tracing.js CHANGED
@@ -1,7 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Preload with: NODE_OPTIONS="-r securenow/register"
4
+ * Preload with: node --require securenow/register app.js
5
+ *
6
+ * Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
7
+ * auto-registered via module.register() — no --import flag needed.
8
+ * On Node 18 with "type": "module", add the hook manually:
9
+ * node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
5
10
  *
6
11
  * Env:
7
12
  * SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
@@ -11,6 +16,7 @@
11
16
  * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
12
17
  * OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
13
18
  * SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
19
+ * SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
14
20
  * OTEL_LOG_LEVEL=info|debug
15
21
  * SECURENOW_TEST_SPAN=1
16
22
  *
@@ -18,15 +24,15 @@
18
24
  * SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
19
25
  */
20
26
 
21
- const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
27
+ const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
22
28
  const { NodeSDK } = require('@opentelemetry/sdk-node');
23
29
  const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
24
30
  const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
25
31
  const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
26
- const { logs: apiLogs } = require('@opentelemetry/api-logs');
27
32
  const { Resource } = require('@opentelemetry/resources');
28
33
  const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
29
34
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
35
+ const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
30
36
  const { v4: uuidv4 } = require('uuid');
31
37
 
32
38
  const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
@@ -47,6 +53,10 @@ const DEFAULT_SENSITIVE_FIELDS = [
47
53
  'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
48
54
  ];
49
55
 
56
+ function escapeRegex(str) {
57
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ }
59
+
50
60
  /**
51
61
  * Redact sensitive fields from an object
52
62
  */
@@ -55,7 +65,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
55
65
 
56
66
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
57
67
 
58
- for (const key in redacted) {
68
+ for (const key of Object.keys(redacted)) {
59
69
  const lowerKey = key.toLowerCase();
60
70
 
61
71
  if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
@@ -78,10 +88,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
78
88
 
79
89
  // Redact sensitive fields in GraphQL arguments and variables
80
90
  sensitiveFields.forEach(field => {
81
- // Match patterns: field: "value" or field: 'value' or field:"value"
91
+ const escaped = escapeRegex(field);
82
92
  const patterns = [
83
- new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
84
- new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
93
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
94
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
85
95
  ];
86
96
 
87
97
  patterns.forEach(pattern => {
@@ -98,26 +108,185 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
98
108
  return redacted;
99
109
  }
100
110
 
111
+ // -------- Multipart streaming parser --------
112
+ // Streams through the request without buffering file content.
113
+ // Only part headers and text-field values are kept in memory,
114
+ // so memory stays bounded (~few KB) regardless of upload size.
115
+
116
+ function extractBoundary(contentType) {
117
+ const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
118
+ return match ? (match[1] || match[2]) : null;
119
+ }
120
+
121
+ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
122
+ const boundary = extractBoundary(contentType);
123
+ if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
124
+
125
+ const result = { fields: Object.create(null), files: [] };
126
+ let totalSize = 0;
127
+ let buf = Buffer.alloc(0);
128
+
129
+ const MAX_PARTS = 100;
130
+ let partCount = 0;
131
+
132
+ const FIRST_DELIM = Buffer.from('--' + boundary);
133
+ const DELIM = Buffer.from('\r\n--' + boundary);
134
+ const HDR_END = Buffer.from('\r\n\r\n');
135
+
136
+ let initialized = false;
137
+ let inHeaders = true;
138
+ let isFile = false;
139
+ let fldName = '';
140
+ let fName = '';
141
+ let pCT = '';
142
+ let bodyBytes = 0;
143
+ let textVal = '';
144
+
145
+ function flushPart() {
146
+ if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
147
+ if (isFile) {
148
+ result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
149
+ } else {
150
+ const lower = fldName.toLowerCase();
151
+ const redact = sensitiveFields.some(f => lower.includes(f.toLowerCase()));
152
+ result.fields[fldName] = redact ? '[REDACTED]' : textVal.substring(0, maxTextFieldSize);
153
+ }
154
+ fldName = ''; bodyBytes = 0; textVal = ''; partCount++;
155
+ }
156
+
157
+ function drain() {
158
+ if (!initialized) {
159
+ const i = buf.indexOf(FIRST_DELIM);
160
+ if (i === -1) {
161
+ if (buf.length > FIRST_DELIM.length + 4) buf = buf.slice(buf.length - FIRST_DELIM.length - 4);
162
+ return;
163
+ }
164
+ buf = buf.slice(i + FIRST_DELIM.length);
165
+ initialized = true;
166
+ inHeaders = true;
167
+ }
168
+
169
+ let guard = 200;
170
+ while (buf.length > 0 && guard-- > 0 && partCount < MAX_PARTS) {
171
+ if (inHeaders) {
172
+ if (buf.length >= 2 && buf[0] === 0x2D && buf[1] === 0x2D) { buf = Buffer.alloc(0); return; }
173
+ if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) { buf = buf.slice(2); continue; }
174
+
175
+ const hi = buf.indexOf(HDR_END);
176
+ if (hi === -1) return;
177
+
178
+ const hdr = buf.slice(0, hi).toString('latin1');
179
+ buf = buf.slice(hi + 4);
180
+
181
+ const nm = hdr.match(/name="([^"]+)"/);
182
+ const fn = hdr.match(/filename="([^"]*)"/);
183
+ const ct = hdr.match(/Content-Type:\s*(.+)/i);
184
+ fldName = nm ? nm[1] : '';
185
+ fName = fn ? fn[1] : '';
186
+ pCT = ct ? ct[1].trim() : '';
187
+ isFile = !!fn;
188
+ bodyBytes = 0;
189
+ textVal = '';
190
+ inHeaders = false;
191
+ }
192
+
193
+ const di = buf.indexOf(DELIM);
194
+ if (di === -1) {
195
+ const safe = Math.max(0, buf.length - DELIM.length - 2);
196
+ if (safe > 0) {
197
+ bodyBytes += safe;
198
+ if (!isFile && textVal.length < maxTextFieldSize) {
199
+ textVal += buf.slice(0, safe).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
200
+ }
201
+ buf = buf.slice(safe);
202
+ }
203
+ return;
204
+ }
205
+
206
+ bodyBytes += di;
207
+ if (!isFile && textVal.length < maxTextFieldSize) {
208
+ textVal += buf.slice(0, di).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
209
+ }
210
+ flushPart();
211
+ buf = buf.slice(di + DELIM.length);
212
+ inHeaders = true;
213
+ }
214
+ }
215
+
216
+ request.on('data', (chunk) => {
217
+ totalSize += chunk.length;
218
+ buf = Buffer.concat([buf, chunk]);
219
+ drain();
220
+ });
221
+
222
+ request.on('end', () => {
223
+ try {
224
+ if (!inHeaders && fldName) {
225
+ bodyBytes += buf.length;
226
+ if (!isFile) textVal += buf.toString('utf8').substring(0, maxTextFieldSize - textVal.length);
227
+ flushPart();
228
+ }
229
+ onComplete({ parsed: result, totalSize });
230
+ } catch (e) {
231
+ onComplete({ error: 'PARSE_ERROR' });
232
+ }
233
+ });
234
+ }
235
+
236
+ // -------- ESM detection --------
237
+ // register.js auto-registers the hook via module.register() on Node >=20.6.
238
+ // This warning only fires if BOTH --import AND module.register() were skipped
239
+ // (e.g. Node 18, or require('securenow/tracing') called directly without register.js).
240
+ (() => {
241
+ try {
242
+ const fs = require('fs');
243
+ const path = require('path');
244
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
245
+ if (fs.existsSync(pkgPath)) {
246
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
247
+ if (pkg.type === 'module') {
248
+ const execArgv = process.execArgv.join(' ');
249
+ const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
250
+ const hasModuleRegister = typeof require('node:module').register === 'function';
251
+ if (!hasCliHook && !hasModuleRegister) {
252
+ console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
253
+ console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
254
+ }
255
+ }
256
+ }
257
+ } catch (_) {}
258
+ })();
259
+
101
260
  // -------- diagnostics --------
102
261
  const diagLevel = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
103
262
  (() => {
104
- const L = diagLevel;
105
- const level = L === 'debug' ? DiagLogLevel.DEBUG :
106
- L === 'info' ? DiagLogLevel.INFO :
107
- L === 'warn' ? DiagLogLevel.WARN :
108
- L === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
263
+ const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
264
+ diagLevel === 'info' ? DiagLogLevel.INFO :
265
+ diagLevel === 'warn' ? DiagLogLevel.WARN :
266
+ diagLevel === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
109
267
  diag.setLogger(new DiagConsoleLogger(), level);
110
268
  console.log('[securenow] preload loaded pid=%d', process.pid);
111
269
  })();
112
270
 
113
- // -------- endpoints --------
114
- const endpointBase = (env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'https://freetrial.securenow.ai:4318').replace(/\/$/, '');
271
+ // -------- endpoints & app resolution --------
272
+ // Resolution order for endpoint/appId/apiKey: env .securenow/credentials.json → package.json#name → defaults.
273
+ const appConfig = require('./app-config');
274
+ const resolvedApp = appConfig.resolveAll();
275
+
276
+ const endpointBase = resolvedApp.instance.replace(/\/$/, '');
115
277
  const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
116
278
  const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
279
+
280
+ // If the credentials file provided an app key and no OTLP headers are set,
281
+ // surface it as x-api-key so the collector can route telemetry to the right app bucket.
282
+ if (resolvedApp.appKey && !env('OTEL_EXPORTER_OTLP_HEADERS') && !env('SECURENOW_API_KEY')) {
283
+ process.env.SECURENOW_API_KEY = resolvedApp.appKey;
284
+ process.env.OTEL_EXPORTER_OTLP_HEADERS = `x-api-key=${resolvedApp.appKey}`;
285
+ }
117
286
  const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
118
287
 
119
288
  // -------- naming rules --------
120
- const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
289
+ const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
121
290
  const baseName = rawBase || null;
122
291
  const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
123
292
  const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
@@ -144,12 +313,11 @@ const instancePrefix = baseName || 'securenow';
144
313
  const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
145
314
 
146
315
  // Loud line per worker to prove what was used
147
- console.log('[securenow] pid=%d SECURENOW_APPID=%s OTEL_SERVICE_NAME=%s SECURENOW_NO_UUID=%s SECURENOW_STRICT=%s → service.name=%s instance.id=%s',
316
+ console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=%s instance.id=%s',
148
317
  process.pid,
149
- JSON.stringify(env('SECURENOW_APPID')),
150
- JSON.stringify(env('OTEL_SERVICE_NAME')),
151
- JSON.stringify(env('SECURENOW_NO_UUID')),
152
- JSON.stringify(env('SECURENOW_STRICT')),
318
+ JSON.stringify(baseName),
319
+ JSON.stringify(endpointBase),
320
+ resolvedApp.appKey ? 'set' : 'none',
153
321
  serviceName,
154
322
  serviceInstanceId
155
323
  );
@@ -161,22 +329,46 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
161
329
  }
162
330
 
163
331
  // -------- Body Capture Configuration --------
164
- const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
165
- const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
332
+ // Opt-out defaults: set =0 or =false to disable.
333
+ const captureBody = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
334
+ const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
166
335
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
167
336
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
168
337
 
338
+ const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
339
+
340
+ // -------- Trusted proxy IP resolution --------
341
+ const { resolveClientIp, isFromTrustedProxy, LOOPBACK_RE } = require('./resolve-ip');
342
+
169
343
  // Configure HTTP instrumentation with body capture
170
344
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
171
345
  const httpInstrumentation = new HttpInstrumentation({
172
346
  requestHook: (span, request) => {
173
347
  try {
174
- if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
348
+ const clientIp = resolveClientIp(request);
349
+ if (clientIp) {
350
+ span.setAttribute('http.client_ip', clientIp);
351
+ }
352
+
353
+ if (request.headers) {
354
+ const SKIP_HEADERS = new Set(['cookie', 'authorization', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-auth-token']);
355
+ const safe = {};
356
+ for (const [k, v] of Object.entries(request.headers)) {
357
+ if (SKIP_HEADERS.has(k.toLowerCase())) { safe[k] = '[REDACTED]'; continue; }
358
+ safe[k] = typeof v === 'string' ? v.substring(0, 500) : String(v);
359
+ }
360
+ const serialized = JSON.stringify(safe);
361
+ if (serialized.length <= 8192) {
362
+ span.setAttribute('http.request.headers', serialized);
363
+ }
364
+ }
365
+
366
+ if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
175
367
  const contentType = request.headers['content-type'] || '';
176
368
 
177
- if (contentType.includes('application/json') ||
369
+ if (captureBody && (contentType.includes('application/json') ||
178
370
  contentType.includes('application/graphql') ||
179
- contentType.includes('application/x-www-form-urlencoded')) {
371
+ contentType.includes('application/x-www-form-urlencoded'))) {
180
372
 
181
373
  let body = '';
182
374
  const chunks = [];
@@ -224,9 +416,9 @@ const httpInstrumentation = new HttpInstrumentation({
224
416
  });
225
417
  }
226
418
  } catch (e) {
227
- // Parse error: capture as-is (truncated)
228
- span.setAttribute('http.request.body', body.substring(0, 1000));
419
+ span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
229
420
  span.setAttribute('http.request.body.parse_error', true);
421
+ span.setAttribute('http.request.body.size', size);
230
422
  }
231
423
  } else if (size > maxBodySize) {
232
424
  span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
@@ -234,10 +426,38 @@ const httpInstrumentation = new HttpInstrumentation({
234
426
  }
235
427
  });
236
428
  } else if (contentType.includes('multipart/form-data')) {
237
- // Multipart is NOT captured
238
- span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
239
- span.setAttribute('http.request.body.type', 'multipart');
240
- span.setAttribute('http.request.body.note', 'File uploads not captured by design');
429
+ if (captureMultipart) {
430
+ collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
431
+ try {
432
+ if (error === 'BOUNDARY_NOT_FOUND') {
433
+ span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
434
+ span.setAttribute('http.request.body.type', 'multipart');
435
+ return;
436
+ }
437
+ if (error) {
438
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
439
+ span.setAttribute('http.request.body.type', 'multipart');
440
+ span.setAttribute('http.request.body.parse_error', true);
441
+ return;
442
+ }
443
+ span.setAttributes({
444
+ 'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
445
+ 'http.request.body.type': 'multipart',
446
+ 'http.request.body.size': totalSize,
447
+ 'http.request.body.fields_count': Object.keys(parsed.fields).length,
448
+ 'http.request.body.files_count': parsed.files.length,
449
+ });
450
+ } catch (e) {
451
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
452
+ span.setAttribute('http.request.body.type', 'multipart');
453
+ span.setAttribute('http.request.body.parse_error', true);
454
+ }
455
+ });
456
+ } else {
457
+ span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
458
+ span.setAttribute('http.request.body.type', 'multipart');
459
+ span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
460
+ }
241
461
  }
242
462
  }
243
463
  } catch (error) {
@@ -247,7 +467,8 @@ const httpInstrumentation = new HttpInstrumentation({
247
467
  });
248
468
 
249
469
  // -------- Logging Configuration --------
250
- const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) !== '0' && String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() !== 'false';
470
+ // Opt-out default: set =0 or =false to disable.
471
+ const loggingEnabled = !/^(0|false)$/i.test(String(env('SECURENOW_LOGGING_ENABLED') ?? ''));
251
472
 
252
473
  // Create shared resource for both traces and logs
253
474
  const sharedResource = new Resource({
@@ -259,7 +480,6 @@ const sharedResource = new Resource({
259
480
 
260
481
  // Initialize LoggerProvider if logging is enabled
261
482
  let loggerProvider = null;
262
- let globalLogger = null;
263
483
 
264
484
  if (loggingEnabled) {
265
485
  const logExporter = new OTLPLogExporter({
@@ -267,23 +487,42 @@ if (loggingEnabled) {
267
487
  headers
268
488
  });
269
489
 
490
+ const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
270
491
  loggerProvider = new LoggerProvider({
271
492
  resource: sharedResource,
272
493
  });
273
- // sdk-logs 0.47.x ignores the `processors` constructor option (added in 0.52),
274
- // so the provider would silently keep a NoopLogRecordProcessor and drop every
275
- // emit(). Register the processor explicitly instead.
276
- loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
277
- apiLogs.setGlobalLoggerProvider(loggerProvider);
494
+ loggerProvider.addLogRecordProcessor(batchLogProcessor);
278
495
 
279
- globalLogger = loggerProvider.getLogger('securenow', '1.0.0');
496
+ // Auto-patch console.* so every log/warn/error becomes an OTel log record
497
+ const _logger = loggerProvider.getLogger('console', '1.0.0');
498
+ const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
499
+ const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
500
+ function _emit(sn, st, args) {
501
+ try {
502
+ const activeCtx = context.active();
503
+ const spanCtx = trace.getSpanContext(activeCtx);
504
+ _logger.emit({
505
+ severityNumber: sn,
506
+ severityText: st,
507
+ body: args.map(a => (typeof a === 'object' && a !== null) ? JSON.stringify(a) : String(a)).join(' '),
508
+ attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
509
+ ...(spanCtx && { context: activeCtx }),
510
+ });
511
+ } catch (_) {}
512
+ }
513
+ console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
514
+ console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
515
+ console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
516
+ console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
517
+ console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
518
+ console.__securenow_patched = true;
280
519
  }
281
520
 
282
521
  // -------- Guard against OTLP exporter socket errors --------
283
522
  // The OTLP HTTP exporter uses keep-alive connections that can be reset by the
284
523
  // remote end (ECONNRESET / "socket hang up"). These transient errors sometimes
285
524
  // escape as unhandled exceptions or rejections because the underlying HTTP
286
- // request's error path is not fully covered by the OTel library. We install
525
+ // request's error path isn't fully covered by the OTel library. We install
287
526
  // targeted process-level handlers to catch them and log at debug level instead
288
527
  // of crashing the host app.
289
528
  const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
@@ -305,8 +544,9 @@ process.on('uncaughtException', (err, origin) => {
305
544
  if (diagLevel === 'debug') {
306
545
  console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
307
546
  }
308
- return;
547
+ return; // swallow — do not crash
309
548
  }
549
+ // Not ours — re-throw so the default handler (or the app's own handler) fires
310
550
  throw err;
311
551
  });
312
552
  process.on('unhandledRejection', (reason) => {
@@ -314,8 +554,9 @@ process.on('unhandledRejection', (reason) => {
314
554
  if (diagLevel === 'debug') {
315
555
  console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
316
556
  }
317
- return;
557
+ return; // swallow
318
558
  }
559
+ // Not ours — re-throw as unhandled so Node's default behaviour applies
319
560
  throw reason;
320
561
  });
321
562
 
@@ -325,9 +566,11 @@ const sdk = new NodeSDK({
325
566
  traceExporter,
326
567
  instrumentations: [
327
568
  httpInstrumentation,
569
+ ...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
328
570
  ...getNodeAutoInstrumentations({
329
571
  ...disabledMap,
330
- '@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
572
+ '@opentelemetry/instrumentation-http': { enabled: false },
573
+ '@opentelemetry/instrumentation-mongodb': { enabled: false },
331
574
  }),
332
575
  ],
333
576
  resource: sharedResource,
@@ -341,27 +584,57 @@ const sdk = new NodeSDK({
341
584
  if (loggingEnabled) {
342
585
  console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
343
586
  } else {
344
- console.log('[securenow] 📋 Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)');
587
+ console.log('[securenow] 📋 Logging: DISABLED (SECURENOW_LOGGING_ENABLED=0)');
345
588
  }
346
589
  if (captureBody) {
347
590
  console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
348
591
  }
592
+ if (captureMultipart) {
593
+ console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
594
+ }
349
595
  if (String(env('SECURENOW_TEST_SPAN')) === '1') {
350
596
  const api = require('@opentelemetry/api');
351
597
  const tracer = api.trace.getTracer('securenow-smoke');
352
598
  const span = tracer.startSpan('securenow.startup.smoke'); span.end();
353
599
  }
600
+
601
+ // Free trial banner
602
+ const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
603
+ if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
604
+ patchHttpForBanner();
605
+ }
606
+
607
+ // Firewall — auto-activates when SECURENOW_API_KEY is set
608
+ const firewallApiKey = env('SECURENOW_API_KEY');
609
+ if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
610
+ require('./firewall').init({
611
+ apiKey: firewallApiKey,
612
+ apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
613
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
614
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
615
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
616
+ statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
617
+ log: env('SECURENOW_FIREWALL_LOG') !== '0',
618
+ tcp: env('SECURENOW_FIREWALL_TCP') === '1',
619
+ iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
620
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
621
+ });
622
+ }
354
623
  } catch (e) {
355
624
  console.error('[securenow] OTel start failed:', e && e.stack || e);
356
625
  }
357
626
  })();
358
627
 
628
+ let shuttingDown = false;
359
629
  async function safeShutdown(sig) {
630
+ if (shuttingDown) return;
631
+ shuttingDown = true;
360
632
  try {
361
633
  await Promise.resolve(sdk.shutdown?.());
362
634
  if (loggerProvider) {
363
635
  await Promise.resolve(loggerProvider.shutdown?.());
364
636
  }
637
+ try { require('./firewall').shutdown(); } catch (_) {}
365
638
  console.log(`[securenow] Tracing and logging terminated on ${sig}`);
366
639
  }
367
640
  catch (e) { console.error('[securenow] Shutdown error:', e); }
@@ -375,7 +648,7 @@ module.exports = {
375
648
  loggerProvider,
376
649
  getLogger: (name = 'default', version = '1.0.0') => {
377
650
  if (!loggerProvider) {
378
- console.warn('[securenow] Logging is not enabled. Set SECURENOW_LOGGING_ENABLED=1 to enable logging.');
651
+ console.warn('[securenow] Logging is disabled (SECURENOW_LOGGING_ENABLED=0). Remove the override to enable.');
379
652
  return null;
380
653
  }
381
654
  return loggerProvider.getLogger(name, version);