securenow 7.7.11 → 7.7.13

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.
package/cli/init.js CHANGED
@@ -137,6 +137,9 @@ async function initNextJs(dir, project, flags) {
137
137
  fs.writeFileSync(newConfigPath, `/** @type {import('next').NextConfig} */
138
138
  const nextConfig = {
139
139
  serverExternalPackages: ['securenow'],
140
+ outputFileTracingIncludes: {
141
+ '/*': ['./node_modules/securenow/**/*'],
142
+ },
140
143
  };
141
144
 
142
145
  export default nextConfig;
@@ -149,21 +152,47 @@ function patchNextConfig(configPath, major) {
149
152
  const content = fs.readFileSync(configPath, 'utf8');
150
153
  const serverExternalWithSecureNow = /serverExternalPackages\s*:\s*\[[\s\S]*?['"]securenow['"][\s\S]*?\]/m.test(content);
151
154
  const serverComponentsWithSecureNow = /serverComponentsExternalPackages\s*:\s*\[[\s\S]*?['"]securenow['"][\s\S]*?\]/m.test(content);
152
- if (serverExternalWithSecureNow || serverComponentsWithSecureNow || content.includes('withSecureNow(')) {
155
+ const traceIncludeWithSecureNow = /outputFileTracingIncludes\s*:\s*{[\s\S]*?node_modules\/securenow\/\*\*/m.test(content);
156
+ if ((serverExternalWithSecureNow || serverComponentsWithSecureNow || content.includes('withSecureNow(')) && traceIncludeWithSecureNow) {
153
157
  return 'already';
154
158
  }
155
159
 
156
160
  if (major < 15) return 'manual';
157
161
 
162
+ function addTraceInclude(nextContent) {
163
+ if (traceIncludeWithSecureNow || /outputFileTracingIncludes\s*:/.test(nextContent)) return nextContent;
164
+ const insert = ` outputFileTracingIncludes: {\n '/*': ['./node_modules/securenow/**/*'],\n },\n`;
165
+ const patterns = [
166
+ /(const\s+nextConfig\s*=\s*{\s*\r?\n)/,
167
+ /(export\s+default\s+{\s*\r?\n)/,
168
+ /(module\.exports\s*=\s*{\s*\r?\n)/,
169
+ ];
170
+ for (const pattern of patterns) {
171
+ if (pattern.test(nextContent)) return nextContent.replace(pattern, `$1${insert}`);
172
+ }
173
+ return null;
174
+ }
175
+
176
+ if (serverExternalWithSecureNow || serverComponentsWithSecureNow || content.includes('withSecureNow(')) {
177
+ const withTraceInclude = addTraceInclude(content);
178
+ if (withTraceInclude && withTraceInclude !== content) {
179
+ fs.writeFileSync(configPath, withTraceInclude, 'utf8');
180
+ return 'patched';
181
+ }
182
+ return 'manual';
183
+ }
184
+
158
185
  const existingServerExternal = content.match(/serverExternalPackages\s*:\s*\[([\s\S]*?)\]/m);
159
186
  if (existingServerExternal) {
160
187
  const current = existingServerExternal[1].trim().replace(/,\s*$/, '');
161
188
  const replacement = `serverExternalPackages: [${current ? `${current}, ` : ''}'securenow']`;
162
- fs.writeFileSync(configPath, content.replace(existingServerExternal[0], replacement), 'utf8');
189
+ let nextContent = content.replace(existingServerExternal[0], replacement);
190
+ nextContent = addTraceInclude(nextContent) || nextContent;
191
+ fs.writeFileSync(configPath, nextContent, 'utf8');
163
192
  return 'patched';
164
193
  }
165
194
 
166
- const insert = ` serverExternalPackages: ['securenow'],\n`;
195
+ const insert = ` serverExternalPackages: ['securenow'],\n outputFileTracingIncludes: {\n '/*': ['./node_modules/securenow/**/*'],\n },\n`;
167
196
  const patterns = [
168
197
  /(const\s+nextConfig\s*=\s*{\s*\r?\n)/,
169
198
  /(export\s+default\s+{\s*\r?\n)/,
@@ -212,7 +241,7 @@ function printAgentPrompt(kind, filename, major, project) {
212
241
  ? `Merge this into ${filename}: in register(), return unless process.env.NEXT_RUNTIME === "nodejs"; then dynamically import "securenow/nextjs" and "securenow/nextjs-auto-capture" with /* webpackIgnore: true */ so Next does not bundle OpenTelemetry internals. Preserve all existing instrumentation.`
213
242
  : null,
214
243
  kind === 'next-config' && major >= 15
215
- ? `Update ${filename} while preserving existing config: add securenow to serverExternalPackages, e.g. serverExternalPackages: [...(existing || []), "securenow"].`
244
+ ? `Update ${filename} while preserving existing config: add securenow to serverExternalPackages, e.g. serverExternalPackages: [...(existing || []), "securenow"], and include SecureNow runtime files for standalone output with outputFileTracingIncludes: { "/*": ["./node_modules/securenow/**/*"] }.`
216
245
  : null,
217
246
  kind === 'next-config' && major < 15
218
247
  ? `Update ${filename} while preserving existing config: enable experimental.instrumentationHook and add securenow to experimental.serverComponentsExternalPackages.`
@@ -97,7 +97,8 @@ var BANNER_SCRIPT =
97
97
  */
98
98
  function patchHttpForBanner() {
99
99
  try {
100
- var http = require('http');
100
+ var nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
101
+ var http = nodeRequire('node:http');
101
102
  var _origWrite = http.ServerResponse.prototype.write;
102
103
  var _origEnd = http.ServerResponse.prototype.end;
103
104
 
package/nextjs.js CHANGED
@@ -34,6 +34,15 @@ const env = appConfig.env;
34
34
 
35
35
  let isRegistered = false;
36
36
 
37
+ function requireRuntimeModule(name) {
38
+ const nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
39
+ return nodeRequire(name);
40
+ }
41
+
42
+ function requireNodeBuiltin(name) {
43
+ return requireRuntimeModule(name);
44
+ }
45
+
37
46
  function createResource(attributes) {
38
47
  if (typeof otelResources.resourceFromAttributes === 'function') {
39
48
  return otelResources.resourceFromAttributes(attributes);
@@ -501,7 +510,7 @@ function registerSecureNow(options = {}) {
501
510
 
502
511
  // Auto-log every incoming HTTP request/response
503
512
  try {
504
- const http = require('http');
513
+ const http = requireNodeBuiltin('node:http');
505
514
  const originalEmit = http.Server.prototype.emit;
506
515
  http.Server.prototype.emit = function (event, req, res) {
507
516
  if (event === 'request' && req && res) {
@@ -549,8 +558,8 @@ function registerSecureNow(options = {}) {
549
558
  } catch (_) {}
550
559
 
551
560
  // Graceful shutdown for logs
552
- process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
553
- process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { require('./firewall').shutdown(); } catch (_) {} });
561
+ process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
562
+ process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
554
563
  } catch (e) {
555
564
  console.warn('[securenow] ⚠️ Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
556
565
  }
@@ -563,7 +572,7 @@ function registerSecureNow(options = {}) {
563
572
 
564
573
  // Free trial banner (optional — may not be bundled in standalone builds)
565
574
  try {
566
- const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
575
+ const { isFreeTrial, patchHttpForBanner } = requireRuntimeModule('./free-trial-banner');
567
576
  if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
568
577
  patchHttpForBanner();
569
578
  }
@@ -607,7 +616,7 @@ function registerSecureNow(options = {}) {
607
616
  const firewallOptions = appConfig.resolveFirewallOptions();
608
617
  if (firewallOptions.apiKey && firewallOptions.enabled) {
609
618
  try {
610
- require('./firewall').init({
619
+ requireRuntimeModule('./firewall').init({
611
620
  apiKey: firewallOptions.apiKey,
612
621
  appKey: firewallOptions.appKey,
613
622
  environment: deploymentEnvironment || firewallOptions.environment,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.11",
3
+ "version": "7.7.13",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
package/tracing.js CHANGED
@@ -124,9 +124,9 @@ function extractBoundary(contentType) {
124
124
  return match ? (match[1] || match[2]) : null;
125
125
  }
126
126
 
127
- function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
127
+ function createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete) {
128
128
  const boundary = extractBoundary(contentType);
129
- if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
129
+ if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return null; }
130
130
 
131
131
  const result = { fields: Object.create(null), files: [] };
132
132
  let totalSize = 0;
@@ -219,13 +219,14 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
219
219
  }
220
220
  }
221
221
 
222
- request.on('data', (chunk) => {
223
- totalSize += chunk.length;
224
- buf = Buffer.concat([buf, chunk]);
222
+ function onData(chunk) {
223
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
224
+ totalSize += data.length;
225
+ buf = Buffer.concat([buf, data]);
225
226
  drain();
226
- });
227
+ }
227
228
 
228
- request.on('end', () => {
229
+ function onEnd() {
229
230
  try {
230
231
  if (!inHeaders && fldName) {
231
232
  bodyBytes += buf.length;
@@ -236,7 +237,16 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
236
237
  } catch (e) {
237
238
  onComplete({ error: 'PARSE_ERROR' });
238
239
  }
239
- });
240
+ }
241
+
242
+ return { onData, onEnd };
243
+ }
244
+
245
+ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
246
+ const collector = createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete);
247
+ if (!collector) return;
248
+ request.on('data', collector.onData);
249
+ request.on('end', collector.onEnd);
240
250
  }
241
251
 
242
252
  // -------- ESM detection --------
@@ -342,6 +352,133 @@ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveField
342
352
 
343
353
  const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
344
354
 
355
+ const BODY_CAPTURE_PATCH = Symbol.for('securenow.bodyCapture.emitPatch');
356
+
357
+ function installRequestBodyObserver(span, request, contentType) {
358
+ if (!request || request[BODY_CAPTURE_PATCH]) return;
359
+
360
+ const normalizedContentType = String(contentType || '').toLowerCase();
361
+ const isStructuredBody = captureBody && (
362
+ normalizedContentType.includes('application/json') ||
363
+ normalizedContentType.includes('application/graphql') ||
364
+ normalizedContentType.includes('application/x-www-form-urlencoded')
365
+ );
366
+ const isMultipartBody = normalizedContentType.includes('multipart/form-data');
367
+
368
+ if (!isStructuredBody && !isMultipartBody) return;
369
+
370
+ if (isMultipartBody && !captureMultipart) {
371
+ span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
372
+ span.setAttribute('http.request.body.type', 'multipart');
373
+ span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
374
+ return;
375
+ }
376
+
377
+ let chunks = [];
378
+ let size = 0;
379
+ let structuredDone = false;
380
+
381
+ const multipartCollector = isMultipartBody && captureMultipart
382
+ ? createMultipartMetaCollector(normalizedContentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
383
+ try {
384
+ if (error === 'BOUNDARY_NOT_FOUND') {
385
+ span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
386
+ span.setAttribute('http.request.body.type', 'multipart');
387
+ return;
388
+ }
389
+ if (error) {
390
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
391
+ span.setAttribute('http.request.body.type', 'multipart');
392
+ span.setAttribute('http.request.body.parse_error', true);
393
+ return;
394
+ }
395
+ span.setAttributes({
396
+ 'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
397
+ 'http.request.body.type': 'multipart',
398
+ 'http.request.body.size': totalSize,
399
+ 'http.request.body.fields_count': Object.keys(parsed.fields).length,
400
+ 'http.request.body.files_count': parsed.files.length,
401
+ });
402
+ } catch (e) {
403
+ span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
404
+ span.setAttribute('http.request.body.type', 'multipart');
405
+ span.setAttribute('http.request.body.parse_error', true);
406
+ }
407
+ })
408
+ : null;
409
+
410
+ if (isMultipartBody && captureMultipart && !multipartCollector) return;
411
+
412
+ function finishStructuredCapture() {
413
+ if (!isStructuredBody || structuredDone) return;
414
+ structuredDone = true;
415
+
416
+ if (size > maxBodySize) {
417
+ span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
418
+ span.setAttribute('http.request.body.size', size);
419
+ chunks = [];
420
+ return;
421
+ }
422
+
423
+ if (chunks.length === 0) return;
424
+
425
+ const body = Buffer.concat(chunks).toString('utf8');
426
+ chunks = [];
427
+
428
+ try {
429
+ if (normalizedContentType.includes('application/graphql')) {
430
+ const redacted = redactGraphQLQuery(body, allSensitiveFields);
431
+ span.setAttributes({
432
+ 'http.request.body': redacted.substring(0, maxBodySize),
433
+ 'http.request.body.type': 'graphql',
434
+ 'http.request.body.size': size,
435
+ });
436
+ } else if (normalizedContentType.includes('application/json')) {
437
+ const parsed = JSON.parse(body);
438
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
439
+ span.setAttributes({
440
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
441
+ 'http.request.body.type': 'json',
442
+ 'http.request.body.size': size,
443
+ });
444
+ } else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
445
+ const parsed = Object.fromEntries(new URLSearchParams(body));
446
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
447
+ span.setAttributes({
448
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
449
+ 'http.request.body.type': 'form',
450
+ 'http.request.body.size': size,
451
+ });
452
+ }
453
+ } catch (e) {
454
+ span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
455
+ span.setAttribute('http.request.body.parse_error', true);
456
+ span.setAttribute('http.request.body.size', size);
457
+ }
458
+ }
459
+
460
+ const originalEmit = request.emit;
461
+ request[BODY_CAPTURE_PATCH] = true;
462
+ request.emit = function securenowObservedEmit(event, ...args) {
463
+ try {
464
+ if (event === 'data' && args.length > 0) {
465
+ const chunk = Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]);
466
+ if (isStructuredBody) {
467
+ size += chunk.length;
468
+ if (size <= maxBodySize) chunks.push(chunk);
469
+ }
470
+ if (multipartCollector) multipartCollector.onData(chunk);
471
+ } else if (event === 'end') {
472
+ finishStructuredCapture();
473
+ if (multipartCollector) multipartCollector.onEnd();
474
+ }
475
+ } catch (_) {
476
+ // Body capture must never interfere with the application request stream.
477
+ }
478
+ return originalEmit.apply(this, [event, ...args]);
479
+ };
480
+ }
481
+
345
482
  // -------- Trusted proxy IP resolution --------
346
483
  const { resolveClientIpWithDetails } = require('./resolve-ip');
347
484
 
@@ -391,100 +528,7 @@ const httpInstrumentation = new HttpInstrumentation({
391
528
 
392
529
  if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
393
530
  const contentType = request.headers['content-type'] || '';
394
-
395
- if (captureBody && (contentType.includes('application/json') ||
396
- contentType.includes('application/graphql') ||
397
- contentType.includes('application/x-www-form-urlencoded'))) {
398
-
399
- let body = '';
400
- const chunks = [];
401
- let size = 0;
402
-
403
- request.on('data', (chunk) => {
404
- size += chunk.length;
405
- if (size <= maxBodySize) {
406
- chunks.push(chunk);
407
- }
408
- });
409
-
410
- request.on('end', () => {
411
- if (size <= maxBodySize && chunks.length > 0) {
412
- body = Buffer.concat(chunks).toString('utf8');
413
-
414
- try {
415
- let redacted;
416
-
417
- if (contentType.includes('application/graphql')) {
418
- // GraphQL: redact query string
419
- redacted = redactGraphQLQuery(body, allSensitiveFields);
420
- span.setAttributes({
421
- 'http.request.body': redacted.substring(0, maxBodySize),
422
- 'http.request.body.type': 'graphql',
423
- 'http.request.body.size': size,
424
- });
425
- } else if (contentType.includes('application/json')) {
426
- // JSON: parse and redact object
427
- const parsed = JSON.parse(body);
428
- redacted = redactSensitiveData(parsed, allSensitiveFields);
429
- span.setAttributes({
430
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
431
- 'http.request.body.type': 'json',
432
- 'http.request.body.size': size,
433
- });
434
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
435
- // Form: parse and redact
436
- const parsed = Object.fromEntries(new URLSearchParams(body));
437
- redacted = redactSensitiveData(parsed, allSensitiveFields);
438
- span.setAttributes({
439
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
440
- 'http.request.body.type': 'form',
441
- 'http.request.body.size': size,
442
- });
443
- }
444
- } catch (e) {
445
- span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
446
- span.setAttribute('http.request.body.parse_error', true);
447
- span.setAttribute('http.request.body.size', size);
448
- }
449
- } else if (size > maxBodySize) {
450
- span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
451
- span.setAttribute('http.request.body.size', size);
452
- }
453
- });
454
- } else if (contentType.includes('multipart/form-data')) {
455
- if (captureMultipart) {
456
- collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
457
- try {
458
- if (error === 'BOUNDARY_NOT_FOUND') {
459
- span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
460
- span.setAttribute('http.request.body.type', 'multipart');
461
- return;
462
- }
463
- if (error) {
464
- span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
465
- span.setAttribute('http.request.body.type', 'multipart');
466
- span.setAttribute('http.request.body.parse_error', true);
467
- return;
468
- }
469
- span.setAttributes({
470
- 'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
471
- 'http.request.body.type': 'multipart',
472
- 'http.request.body.size': totalSize,
473
- 'http.request.body.fields_count': Object.keys(parsed.fields).length,
474
- 'http.request.body.files_count': parsed.files.length,
475
- });
476
- } catch (e) {
477
- span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
478
- span.setAttribute('http.request.body.type', 'multipart');
479
- span.setAttribute('http.request.body.parse_error', true);
480
- }
481
- });
482
- } else {
483
- span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
484
- span.setAttribute('http.request.body.type', 'multipart');
485
- span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
486
- }
487
- }
531
+ installRequestBodyObserver(span, request, contentType);
488
532
  }
489
533
  } catch (error) {
490
534
  // Silently fail