securenow 4.0.3 → 4.0.5
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/AUTO-BODY-CAPTURE.md +409 -0
- package/BODY-CAPTURE-FIX.md +258 -0
- package/CUSTOMER-GUIDE.md +5 -1
- package/EASIEST-SETUP.md +339 -0
- package/FINAL-SOLUTION.md +332 -0
- package/NEXTJS-BODY-CAPTURE-COMPARISON.md +320 -0
- package/NEXTJS-BODY-CAPTURE.md +368 -0
- package/NEXTJS-WRAPPER-APPROACH.md +411 -0
- package/QUICKSTART-BODY-CAPTURE.md +287 -0
- package/SOLUTION-SUMMARY.md +309 -0
- package/cli.js +1 -1
- package/examples/instrumentation-with-auto-capture.ts +38 -0
- package/examples/nextjs-api-route-with-body-capture.ts +51 -0
- package/examples/nextjs-middleware.js +34 -0
- package/examples/nextjs-middleware.ts +34 -0
- package/nextjs-auto-capture.js +204 -0
- package/nextjs-middleware.js +178 -0
- package/nextjs-wrapper.js +155 -0
- package/nextjs.js +24 -61
- package/package.json +17 -2
- package/postinstall.js +117 -22
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Next.js Middleware for Body Capture
|
|
3
|
+
*
|
|
4
|
+
* OPTIONAL: Import this in your Next.js app to enable automatic body capture
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* Create middleware.ts in your Next.js app root:
|
|
9
|
+
*
|
|
10
|
+
* export { middleware } from 'securenow/nextjs-middleware';
|
|
11
|
+
* export const config = {
|
|
12
|
+
* matcher: '/api/:path*', // Apply to API routes only
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* That's it! Bodies are now captured with sensitive data redacted.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
|
|
19
|
+
|
|
20
|
+
// Default sensitive fields to redact
|
|
21
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
22
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
23
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
24
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Redact sensitive fields from an object
|
|
29
|
+
*/
|
|
30
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
31
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
32
|
+
|
|
33
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
34
|
+
|
|
35
|
+
for (const key in redacted) {
|
|
36
|
+
const lowerKey = key.toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
39
|
+
redacted[key] = '[REDACTED]';
|
|
40
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
41
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return redacted;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Redact sensitive data from GraphQL query strings
|
|
50
|
+
*/
|
|
51
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
52
|
+
if (!query || typeof query !== 'string') return query;
|
|
53
|
+
|
|
54
|
+
let redacted = query;
|
|
55
|
+
|
|
56
|
+
sensitiveFields.forEach(field => {
|
|
57
|
+
const patterns = [
|
|
58
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
59
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
patterns.forEach(pattern => {
|
|
63
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
64
|
+
return suffix ? `${prefix}[REDACTED]${suffix}` : `${prefix}[REDACTED]`;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return redacted;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Next.js Middleware for Body Capture
|
|
74
|
+
*/
|
|
75
|
+
async function middleware(request) {
|
|
76
|
+
const { NextResponse } = require('next/server');
|
|
77
|
+
|
|
78
|
+
// Only capture for POST/PUT/PATCH
|
|
79
|
+
if (!['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
80
|
+
return NextResponse.next();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get or create a tracer
|
|
84
|
+
const tracer = trace.getTracer('securenow-middleware');
|
|
85
|
+
let span = trace.getActiveSpan();
|
|
86
|
+
let createdSpan = false;
|
|
87
|
+
|
|
88
|
+
// If no active span, create one for this middleware
|
|
89
|
+
if (!span) {
|
|
90
|
+
const url = new URL(request.url);
|
|
91
|
+
span = tracer.startSpan(`middleware ${request.method} ${url.pathname}`);
|
|
92
|
+
createdSpan = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const contentType = request.headers.get('content-type') || '';
|
|
97
|
+
const maxBodySize = parseInt(process.env.SECURENOW_MAX_BODY_SIZE || '10240');
|
|
98
|
+
const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
99
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
100
|
+
|
|
101
|
+
// Only capture supported types
|
|
102
|
+
if (contentType.includes('application/json') ||
|
|
103
|
+
contentType.includes('application/graphql')) {
|
|
104
|
+
|
|
105
|
+
// Clone the request to read body without consuming the original
|
|
106
|
+
const clonedRequest = request.clone();
|
|
107
|
+
const bodyText = await clonedRequest.text();
|
|
108
|
+
|
|
109
|
+
if (bodyText.length <= maxBodySize) {
|
|
110
|
+
let redactedBody;
|
|
111
|
+
|
|
112
|
+
if (contentType.includes('application/graphql')) {
|
|
113
|
+
// GraphQL: redact query string
|
|
114
|
+
redactedBody = redactGraphQLQuery(bodyText, allSensitiveFields);
|
|
115
|
+
} else {
|
|
116
|
+
// JSON: parse and redact
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(bodyText);
|
|
119
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
120
|
+
redactedBody = JSON.stringify(redacted);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
redactedBody = bodyText; // Keep as-is if parse fails
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
span.setAttributes({
|
|
127
|
+
'http.request.body': redactedBody.substring(0, maxBodySize),
|
|
128
|
+
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
129
|
+
'http.request.body.size': bodyText.length,
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
133
|
+
}
|
|
134
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
135
|
+
const clonedRequest = request.clone();
|
|
136
|
+
const formData = await clonedRequest.formData();
|
|
137
|
+
const parsed = Object.fromEntries(formData);
|
|
138
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
139
|
+
|
|
140
|
+
span.setAttributes({
|
|
141
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
142
|
+
'http.request.body.type': 'form',
|
|
143
|
+
'http.request.body.size': JSON.stringify(parsed).length,
|
|
144
|
+
});
|
|
145
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
146
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
147
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// End span if we created it
|
|
151
|
+
if (createdSpan) {
|
|
152
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
153
|
+
span.end();
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// Silently fail - don't break the request
|
|
157
|
+
console.debug('[securenow] Body capture failed:', error.message);
|
|
158
|
+
|
|
159
|
+
// End span with error if we created it
|
|
160
|
+
if (createdSpan && span) {
|
|
161
|
+
span.setStatus({
|
|
162
|
+
code: SpanStatusCode.ERROR,
|
|
163
|
+
message: error.message
|
|
164
|
+
});
|
|
165
|
+
span.end();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return NextResponse.next();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
middleware,
|
|
174
|
+
redactSensitiveData,
|
|
175
|
+
redactGraphQLQuery,
|
|
176
|
+
DEFAULT_SENSITIVE_FIELDS,
|
|
177
|
+
};
|
|
178
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Next.js API Route Wrapper for Body Capture
|
|
3
|
+
*
|
|
4
|
+
* This approach is NON-INVASIVE and runs INSIDE your handler,
|
|
5
|
+
* so it never blocks or interferes with middleware or routing.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* import { withSecureNow } from 'securenow/nextjs-wrapper';
|
|
10
|
+
*
|
|
11
|
+
* export const POST = withSecureNow(async (request) => {
|
|
12
|
+
* // Your handler code - request.body is available as parsed JSON
|
|
13
|
+
* const data = await request.json();
|
|
14
|
+
* return Response.json({ success: true });
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { trace } = require('@opentelemetry/api');
|
|
19
|
+
|
|
20
|
+
// Default sensitive fields to redact
|
|
21
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
22
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
23
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
24
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Redact sensitive fields from an object
|
|
29
|
+
*/
|
|
30
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
31
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
32
|
+
|
|
33
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
34
|
+
|
|
35
|
+
for (const key in redacted) {
|
|
36
|
+
const lowerKey = key.toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
39
|
+
redacted[key] = '[REDACTED]';
|
|
40
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
41
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return redacted;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Capture body from Request object (clone to avoid consuming)
|
|
50
|
+
*/
|
|
51
|
+
async function captureRequestBody(request) {
|
|
52
|
+
const captureBody = String(process.env.SECURENOW_CAPTURE_BODY) === '1' ||
|
|
53
|
+
String(process.env.SECURENOW_CAPTURE_BODY).toLowerCase() === 'true';
|
|
54
|
+
|
|
55
|
+
if (!captureBody) return;
|
|
56
|
+
if (!['POST', 'PUT', 'PATCH'].includes(request.method)) return;
|
|
57
|
+
|
|
58
|
+
const span = trace.getActiveSpan();
|
|
59
|
+
if (!span) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const contentType = request.headers.get('content-type') || '';
|
|
63
|
+
const maxBodySize = parseInt(process.env.SECURENOW_MAX_BODY_SIZE || '10240');
|
|
64
|
+
const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
65
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
66
|
+
|
|
67
|
+
// Only for supported types
|
|
68
|
+
if (!contentType.includes('application/json') &&
|
|
69
|
+
!contentType.includes('application/graphql') &&
|
|
70
|
+
!contentType.includes('application/x-www-form-urlencoded')) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clone to avoid consuming the original
|
|
75
|
+
const cloned = request.clone();
|
|
76
|
+
const bodyText = await cloned.text();
|
|
77
|
+
|
|
78
|
+
if (bodyText.length > maxBodySize) {
|
|
79
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
80
|
+
span.setAttribute('http.request.body.size', bodyText.length);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Parse and redact based on type
|
|
85
|
+
let redacted;
|
|
86
|
+
if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(bodyText);
|
|
89
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
90
|
+
span.setAttributes({
|
|
91
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
92
|
+
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
93
|
+
'http.request.body.size': bodyText.length,
|
|
94
|
+
});
|
|
95
|
+
} catch (e) {
|
|
96
|
+
span.setAttribute('http.request.body', '[INVALID JSON]');
|
|
97
|
+
}
|
|
98
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
99
|
+
const params = new URLSearchParams(bodyText);
|
|
100
|
+
const parsed = Object.fromEntries(params);
|
|
101
|
+
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
102
|
+
span.setAttributes({
|
|
103
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
104
|
+
'http.request.body.type': 'form',
|
|
105
|
+
'http.request.body.size': bodyText.length,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Silently fail - never block the request
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wrap a Next.js API route handler to capture body
|
|
115
|
+
* This is OPTIONAL and NON-INVASIVE - only use on routes where you want body capture
|
|
116
|
+
*/
|
|
117
|
+
function withSecureNow(handler) {
|
|
118
|
+
return async function wrappedHandler(request, context) {
|
|
119
|
+
// Capture body asynchronously (doesn't block handler)
|
|
120
|
+
captureRequestBody(request).catch(() => {
|
|
121
|
+
// Ignore errors silently
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Call original handler immediately - no blocking!
|
|
125
|
+
return handler(request, context);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Alternative: Auto-capture wrapper that tries to capture AFTER handler runs
|
|
131
|
+
* This is even safer as it never interferes with the handler logic
|
|
132
|
+
*/
|
|
133
|
+
function withSecureNowAsync(handler) {
|
|
134
|
+
return async function wrappedHandler(request, context) {
|
|
135
|
+
// Try to capture body in background (non-blocking)
|
|
136
|
+
const capturePromise = captureRequestBody(request);
|
|
137
|
+
|
|
138
|
+
// Run handler
|
|
139
|
+
const response = await handler(request, context);
|
|
140
|
+
|
|
141
|
+
// Wait for capture to finish (but don't fail if it doesn't)
|
|
142
|
+
await capturePromise.catch(() => {});
|
|
143
|
+
|
|
144
|
+
return response;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
withSecureNow,
|
|
150
|
+
withSecureNowAsync,
|
|
151
|
+
captureRequestBody,
|
|
152
|
+
redactSensitiveData,
|
|
153
|
+
DEFAULT_SENSITIVE_FIELDS,
|
|
154
|
+
};
|
|
155
|
+
|
package/nextjs.js
CHANGED
|
@@ -276,11 +276,18 @@ function registerSecureNow(options = {}) {
|
|
|
276
276
|
'vercel.region': process.env.VERCEL_REGION || undefined,
|
|
277
277
|
},
|
|
278
278
|
instrumentations: [
|
|
279
|
-
// Add HTTP instrumentation with request hooks to capture IP, headers
|
|
279
|
+
// Add HTTP instrumentation with request hooks to capture IP, headers
|
|
280
|
+
// NOTE: Body capture is DISABLED at this level for Next.js to prevent conflicts
|
|
280
281
|
new HttpInstrumentation({
|
|
281
282
|
requireParentforOutgoingSpans: false,
|
|
282
283
|
requireParentforIncomingSpans: false,
|
|
283
|
-
|
|
284
|
+
// Ignore request/response bodies to prevent Next.js conflicts
|
|
285
|
+
ignoreIncomingRequestHook: (request) => {
|
|
286
|
+
// Never ignore - we want to trace all requests
|
|
287
|
+
return false;
|
|
288
|
+
},
|
|
289
|
+
requestHook: (span, request) => {
|
|
290
|
+
// SYNCHRONOUS ONLY - no async operations to avoid timing issues
|
|
284
291
|
try {
|
|
285
292
|
// Capture client IP from various headers
|
|
286
293
|
const headers = request.headers || {};
|
|
@@ -294,7 +301,7 @@ function registerSecureNow(options = {}) {
|
|
|
294
301
|
request.socket?.remoteAddress ||
|
|
295
302
|
'unknown';
|
|
296
303
|
|
|
297
|
-
// Add IP and request metadata to span
|
|
304
|
+
// Add IP and request metadata to span (synchronously)
|
|
298
305
|
span.setAttributes({
|
|
299
306
|
'http.client_ip': clientIp,
|
|
300
307
|
'http.user_agent': headers['user-agent'] || 'unknown',
|
|
@@ -319,64 +326,20 @@ function registerSecureNow(options = {}) {
|
|
|
319
326
|
span.setAttribute('http.geo.country', headers['cf-ipcountry']);
|
|
320
327
|
}
|
|
321
328
|
|
|
322
|
-
// --------
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const bodyResult = await captureRequestBody(request, maxBodySize);
|
|
332
|
-
|
|
333
|
-
if (bodyResult.captured) {
|
|
334
|
-
let redactedBody;
|
|
335
|
-
|
|
336
|
-
// Redact based on type
|
|
337
|
-
if (bodyResult.type === 'graphql') {
|
|
338
|
-
// GraphQL: redact query string
|
|
339
|
-
redactedBody = redactGraphQLQuery(bodyResult.body, allSensitiveFields);
|
|
340
|
-
} else if (typeof bodyResult.body === 'object') {
|
|
341
|
-
// JSON/Form: redact object properties
|
|
342
|
-
redactedBody = redactSensitiveData(bodyResult.body, allSensitiveFields);
|
|
343
|
-
} else {
|
|
344
|
-
// Plain text: basic redaction
|
|
345
|
-
redactedBody = bodyResult.body;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
span.setAttributes({
|
|
349
|
-
'http.request.body': typeof redactedBody === 'string'
|
|
350
|
-
? redactedBody.substring(0, maxBodySize)
|
|
351
|
-
: JSON.stringify(redactedBody).substring(0, maxBodySize),
|
|
352
|
-
'http.request.body.type': bodyResult.type,
|
|
353
|
-
'http.request.body.size': bodyResult.size,
|
|
354
|
-
});
|
|
355
|
-
} else {
|
|
356
|
-
span.setAttribute('http.request.body.capture_failed', bodyResult.reason || 'unknown');
|
|
357
|
-
}
|
|
358
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
359
|
-
// Multipart is NOT captured at all
|
|
360
|
-
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
361
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
362
|
-
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
363
|
-
}
|
|
364
|
-
}
|
|
329
|
+
// -------- Request Body NOT captured at HTTP instrumentation level --------
|
|
330
|
+
// IMPORTANT: Do NOT attempt to read request.body or listen to 'data' events
|
|
331
|
+
// Next.js manages request streams internally and reading them here causes:
|
|
332
|
+
// - "Response body object should not be disturbed or locked" errors
|
|
333
|
+
// - Hanging requests that never complete
|
|
334
|
+
// - Body data unavailable to Next.js route handlers
|
|
335
|
+
//
|
|
336
|
+
// Body capture must be done in Next.js middleware using request.clone()
|
|
337
|
+
|
|
365
338
|
} catch (error) {
|
|
366
339
|
// Silently fail to not break the request
|
|
367
|
-
|
|
340
|
+
// Do not log in production to avoid noise
|
|
368
341
|
}
|
|
369
|
-
}
|
|
370
|
-
responseHook: (span, response) => {
|
|
371
|
-
try {
|
|
372
|
-
// Add response metadata
|
|
373
|
-
if (response.statusCode) {
|
|
374
|
-
span.setAttribute('http.status_code', response.statusCode);
|
|
375
|
-
}
|
|
376
|
-
} catch (error) {
|
|
377
|
-
console.debug('[securenow] Failed to capture response metadata:', error.message);
|
|
378
|
-
}
|
|
379
|
-
},
|
|
342
|
+
}
|
|
380
343
|
}),
|
|
381
344
|
],
|
|
382
345
|
instrumentationConfig: {
|
|
@@ -402,10 +365,10 @@ function registerSecureNow(options = {}) {
|
|
|
402
365
|
isRegistered = true;
|
|
403
366
|
console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
|
|
404
367
|
console.log('[securenow] 📊 Auto-capturing: IP, User-Agent, Headers, Geographic data');
|
|
368
|
+
console.log('[securenow] ⚠️ Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
|
|
405
369
|
if (captureBody) {
|
|
406
|
-
console.log('[securenow]
|
|
407
|
-
|
|
408
|
-
console.log('[securenow] 📝 Request body capture: DISABLED (set SECURENOW_CAPTURE_BODY=1 to enable)');
|
|
370
|
+
console.log('[securenow] 💡 SECURENOW_CAPTURE_BODY is set but has no effect in Next.js HTTP instrumentation');
|
|
371
|
+
console.log('[securenow] 💡 Body capture must be implemented differently for Next.js (coming soon)');
|
|
409
372
|
}
|
|
410
373
|
|
|
411
374
|
// Optional test span
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js and Next.js - Send traces to SigNoz or any OTLP backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"./register": "./register.js",
|
|
34
34
|
"./tracing": "./tracing.js",
|
|
35
35
|
"./nextjs": "./nextjs.js",
|
|
36
|
+
"./nextjs-auto-capture": "./nextjs-auto-capture.js",
|
|
37
|
+
"./nextjs-middleware": "./nextjs-middleware.js",
|
|
38
|
+
"./nextjs-wrapper": "./nextjs-wrapper.js",
|
|
36
39
|
"./nextjs-webpack-config": "./nextjs-webpack-config.js",
|
|
37
40
|
"./register-vite": "./register-vite.js",
|
|
38
41
|
"./web-vite": {
|
|
@@ -44,6 +47,9 @@
|
|
|
44
47
|
"register.js",
|
|
45
48
|
"tracing.js",
|
|
46
49
|
"nextjs.js",
|
|
50
|
+
"nextjs-auto-capture.js",
|
|
51
|
+
"nextjs-middleware.js",
|
|
52
|
+
"nextjs-wrapper.js",
|
|
47
53
|
"cli.js",
|
|
48
54
|
"postinstall.js",
|
|
49
55
|
"register-vite.js",
|
|
@@ -57,7 +63,16 @@
|
|
|
57
63
|
"AUTOMATIC-IP-CAPTURE.md",
|
|
58
64
|
"REQUEST-BODY-CAPTURE.md",
|
|
59
65
|
"BODY-CAPTURE-QUICKSTART.md",
|
|
60
|
-
"REDACTION-EXAMPLES.md"
|
|
66
|
+
"REDACTION-EXAMPLES.md",
|
|
67
|
+
"NEXTJS-BODY-CAPTURE.md",
|
|
68
|
+
"NEXTJS-BODY-CAPTURE-COMPARISON.md",
|
|
69
|
+
"NEXTJS-WRAPPER-APPROACH.md",
|
|
70
|
+
"QUICKSTART-BODY-CAPTURE.md",
|
|
71
|
+
"AUTO-BODY-CAPTURE.md",
|
|
72
|
+
"EASIEST-SETUP.md",
|
|
73
|
+
"SOLUTION-SUMMARY.md",
|
|
74
|
+
"BODY-CAPTURE-FIX.md",
|
|
75
|
+
"FINAL-SOLUTION.md"
|
|
61
76
|
],
|
|
62
77
|
"dependencies": {
|
|
63
78
|
"@opentelemetry/api": "1.7.0",
|
package/postinstall.js
CHANGED
|
@@ -80,6 +80,64 @@ export function register() {
|
|
|
80
80
|
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
81
81
|
* OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
82
82
|
* OTEL_LOG_LEVEL=info
|
|
83
|
+
*
|
|
84
|
+
* Optional: Enable request body capture
|
|
85
|
+
* SECURENOW_CAPTURE_BODY=1
|
|
86
|
+
* (Also create middleware.ts to activate - run: npx securenow init)
|
|
87
|
+
*/
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create TypeScript middleware file
|
|
94
|
+
function createTsMiddleware(targetPath) {
|
|
95
|
+
const content = `// SecureNow Middleware - Automatic Request Body Capture
|
|
96
|
+
// This enables capturing JSON, GraphQL, and Form request bodies
|
|
97
|
+
// with automatic sensitive field redaction
|
|
98
|
+
|
|
99
|
+
export { middleware } from 'securenow/nextjs-middleware';
|
|
100
|
+
|
|
101
|
+
export const config = {
|
|
102
|
+
matcher: '/api/:path*', // Apply to all API routes
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Bodies are captured with:
|
|
107
|
+
* - Automatic redaction of passwords, tokens, cards, etc.
|
|
108
|
+
* - Size limits (configurable via SECURENOW_MAX_BODY_SIZE)
|
|
109
|
+
* - JSON, GraphQL, Form data support
|
|
110
|
+
*
|
|
111
|
+
* Configure in .env.local:
|
|
112
|
+
* SECURENOW_MAX_BODY_SIZE=20480
|
|
113
|
+
* SECURENOW_SENSITIVE_FIELDS=email,phone
|
|
114
|
+
*/
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create JavaScript middleware file
|
|
121
|
+
function createJsMiddleware(targetPath) {
|
|
122
|
+
const content = `// SecureNow Middleware - Automatic Request Body Capture
|
|
123
|
+
// This enables capturing JSON, GraphQL, and Form request bodies
|
|
124
|
+
// with automatic sensitive field redaction
|
|
125
|
+
|
|
126
|
+
export { middleware } from 'securenow/nextjs-middleware';
|
|
127
|
+
|
|
128
|
+
export const config = {
|
|
129
|
+
matcher: '/api/:path*', // Apply to all API routes
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Bodies are captured with:
|
|
134
|
+
* - Automatic redaction of passwords, tokens, cards, etc.
|
|
135
|
+
* - Size limits (configurable via SECURENOW_MAX_BODY_SIZE)
|
|
136
|
+
* - JSON, GraphQL, Form data support
|
|
137
|
+
*
|
|
138
|
+
* Configure in .env.local:
|
|
139
|
+
* SECURENOW_MAX_BODY_SIZE=20480
|
|
140
|
+
* SECURENOW_SENSITIVE_FIELDS=email,phone
|
|
83
141
|
*/
|
|
84
142
|
`;
|
|
85
143
|
|
|
@@ -101,6 +159,11 @@ SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
|
101
159
|
|
|
102
160
|
# Optional: Log level (debug|info|warn|error)
|
|
103
161
|
# OTEL_LOG_LEVEL=info
|
|
162
|
+
|
|
163
|
+
# Optional: Enable request body capture (requires middleware.ts)
|
|
164
|
+
# SECURENOW_CAPTURE_BODY=1
|
|
165
|
+
# SECURENOW_MAX_BODY_SIZE=10240
|
|
166
|
+
# SECURENOW_SENSITIVE_FIELDS=email,phone
|
|
104
167
|
`;
|
|
105
168
|
|
|
106
169
|
fs.writeFileSync(targetPath, content, 'utf8');
|
|
@@ -171,33 +234,65 @@ async function setup() {
|
|
|
171
234
|
|
|
172
235
|
console.log(`\n✅ Created ${srcExists ? 'src/' : ''}${fileName}`);
|
|
173
236
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
// Ask about middleware for body capture
|
|
238
|
+
rl.question('\nWould you like to enable request body capture? (y/N) ', (middlewareAnswer) => {
|
|
239
|
+
const shouldCreateMiddleware = middlewareAnswer && (middlewareAnswer.toLowerCase() === 'y' || middlewareAnswer.toLowerCase() === 'yes');
|
|
240
|
+
|
|
241
|
+
if (shouldCreateMiddleware) {
|
|
242
|
+
try {
|
|
243
|
+
const middlewareName = useTypeScript ? 'middleware.ts' : 'middleware.js';
|
|
244
|
+
const middlewarePath = srcExists
|
|
245
|
+
? path.join(process.cwd(), 'src', middlewareName)
|
|
246
|
+
: path.join(process.cwd(), middlewareName);
|
|
247
|
+
|
|
248
|
+
if (useTypeScript) {
|
|
249
|
+
createTsMiddleware(middlewarePath);
|
|
250
|
+
} else {
|
|
251
|
+
createJsMiddleware(middlewarePath);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`✅ Created ${srcExists ? 'src/' : ''}${middlewareName}`);
|
|
255
|
+
console.log(' → Captures JSON, GraphQL, Form bodies with auto-redaction');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(`⚠️ Could not create middleware: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create .env.local if it doesn't exist
|
|
262
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
263
|
+
if (!fs.existsSync(envPath)) {
|
|
264
|
+
createEnvTemplate(envPath);
|
|
265
|
+
console.log('✅ Created .env.local template');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log('\n┌─────────────────────────────────────────────────┐');
|
|
269
|
+
console.log('│ 🚀 Next Steps: │');
|
|
270
|
+
console.log('│ │');
|
|
271
|
+
console.log('│ 1. Edit .env.local and set: │');
|
|
272
|
+
console.log('│ SECURENOW_APPID=your-app-name │');
|
|
273
|
+
console.log('│ SECURENOW_INSTANCE=http://signoz:4318 │');
|
|
274
|
+
if (shouldCreateMiddleware) {
|
|
275
|
+
console.log('│ SECURENOW_CAPTURE_BODY=1 │');
|
|
276
|
+
}
|
|
277
|
+
console.log('│ │');
|
|
278
|
+
console.log('│ 2. Run your app: npm run dev │');
|
|
279
|
+
console.log('│ │');
|
|
280
|
+
console.log('│ 3. Check SigNoz for traces! │');
|
|
281
|
+
console.log('│ │');
|
|
282
|
+
if (shouldCreateMiddleware) {
|
|
283
|
+
console.log('│ 📝 Body capture enabled with auto-redaction │');
|
|
284
|
+
}
|
|
285
|
+
console.log('│ 📚 Full guide: npm docs securenow │');
|
|
286
|
+
console.log('└─────────────────────────────────────────────────┘\n');
|
|
287
|
+
|
|
288
|
+
rl.close();
|
|
289
|
+
});
|
|
194
290
|
|
|
195
291
|
} catch (error) {
|
|
196
292
|
console.error('\n❌ Failed to create instrumentation file:', error.message);
|
|
197
293
|
console.log('💡 You can create it manually or run: npx securenow init');
|
|
294
|
+
rl.close();
|
|
198
295
|
}
|
|
199
|
-
|
|
200
|
-
rl.close();
|
|
201
296
|
});
|
|
202
297
|
}
|
|
203
298
|
|