securenow 5.10.2 → 5.11.1

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.
@@ -1,199 +1,199 @@
1
- /**
2
- * SecureNow Next.js Automatic Body Capture
3
- *
4
- * This module automatically patches Next.js request handling to capture bodies
5
- * WITHOUT requiring customers to wrap their handlers or change their code.
6
- *
7
- * Usage in instrumentation.ts:
8
- *
9
- * import { registerSecureNow } from 'securenow/nextjs';
10
- * import 'securenow/nextjs-auto-capture'; // Just import this line!
11
- *
12
- * export function register() {
13
- * registerSecureNow();
14
- * }
15
- *
16
- * That's it! Bodies are now captured automatically.
17
- */
18
-
19
- const { trace } = require('@opentelemetry/api');
20
-
21
- // Default sensitive fields to redact
22
- const DEFAULT_SENSITIVE_FIELDS = [
23
- 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
24
- 'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
25
- 'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
26
- ];
27
-
28
- /**
29
- * Redact sensitive fields from an object
30
- */
31
- function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
32
- if (!obj || typeof obj !== 'object') return obj;
33
-
34
- const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
35
-
36
- for (const key of Object.keys(redacted)) {
37
- const lowerKey = key.toLowerCase();
38
-
39
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
40
- redacted[key] = '[REDACTED]';
41
- } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
42
- redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
43
- }
44
- }
45
-
46
- return redacted;
47
- }
48
-
49
- /**
50
- * Safe body capture that doesn't interfere with Next.js
51
- */
52
- async function safeBodyCapture(request, span) {
53
- if (!span) return;
54
-
55
- try {
56
- const contentType = request.headers.get('content-type') || '';
57
- const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
58
- const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
59
- const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
60
-
61
- // Only for supported types
62
- if (!contentType.includes('application/json') &&
63
- !contentType.includes('application/graphql') &&
64
- !contentType.includes('application/x-www-form-urlencoded')) {
65
- return;
66
- }
67
-
68
- // Try to read from cache if available (Next.js may have already read it)
69
- let bodyText;
70
-
71
- // Attempt 1: Check if body was already cached by Next.js
72
- if (request._bodyText) {
73
- bodyText = request._bodyText;
74
- } else {
75
- // Attempt 2: Try to clone and read
76
- try {
77
- const cloned = request.clone();
78
- bodyText = await cloned.text();
79
- // Cache it for Next.js
80
- request._bodyText = bodyText;
81
- } catch (e) {
82
- // If clone fails, body was already consumed - skip silently
83
- return;
84
- }
85
- }
86
-
87
- if (bodyText.length > maxBodySize) {
88
- span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
89
- return;
90
- }
91
-
92
- // Parse and redact
93
- if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
94
- try {
95
- const parsed = JSON.parse(bodyText);
96
- const redacted = redactSensitiveData(parsed, allSensitiveFields);
97
- span.setAttributes({
98
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
99
- 'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
100
- 'http.request.body.size': bodyText.length,
101
- });
102
- } catch (e) {
103
- // Parse error - skip
104
- }
105
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
106
- try {
107
- const params = new URLSearchParams(bodyText);
108
- const parsed = Object.fromEntries(params);
109
- const redacted = redactSensitiveData(parsed, allSensitiveFields);
110
- span.setAttributes({
111
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
112
- 'http.request.body.type': 'form',
113
- 'http.request.body.size': bodyText.length,
114
- });
115
- } catch (e) {
116
- // Parse error - skip
117
- }
118
- }
119
- } catch (error) {
120
- // Silently fail - never break the request
121
- }
122
- }
123
-
124
- /**
125
- * Check if body capture is enabled
126
- */
127
- function isBodyCaptureEnabled() {
128
- const enabled = String(process.env.SECURENOW_CAPTURE_BODY) === '1' ||
129
- String(process.env.SECURENOW_CAPTURE_BODY).toLowerCase() === 'true';
130
- return enabled;
131
- }
132
-
133
- /**
134
- * Patch Next.js Request to cache body text
135
- * This allows us to read the body without consuming it
136
- */
137
- function patchNextRequest() {
138
- if (typeof Request === 'undefined') return;
139
-
140
- const originalText = Request.prototype.text;
141
- const originalJson = Request.prototype.json;
142
-
143
- // Patch text() to cache result
144
- Request.prototype.text = async function() {
145
- if (this._bodyText !== undefined) {
146
- return this._bodyText;
147
- }
148
- const text = await originalText.call(this);
149
- this._bodyText = text;
150
-
151
- // Capture for tracing if enabled
152
- if (isBodyCaptureEnabled() && ['POST', 'PUT', 'PATCH'].includes(this.method)) {
153
- const span = trace.getActiveSpan();
154
- if (span) {
155
- // Schedule capture after this call (non-blocking)
156
- setImmediate(() => {
157
- safeBodyCapture(this, span).catch(() => {});
158
- });
159
- }
160
- }
161
-
162
- return text;
163
- };
164
-
165
- // Patch json() to cache and capture
166
- Request.prototype.json = async function() {
167
- // First get text
168
- const text = await this.text();
169
- // Then parse
170
- return JSON.parse(text);
171
- };
172
-
173
- console.log('[securenow] ✅ Auto-capture: Patched Next.js Request for automatic body capture');
174
- }
175
-
176
- // Auto-patch when module is imported
177
- if (isBodyCaptureEnabled()) {
178
- try {
179
- patchNextRequest();
180
- console.log('[securenow] 📝 Automatic body capture: ENABLED');
181
- console.log('[securenow] 💡 No code changes needed - bodies captured automatically!');
182
- } catch (error) {
183
- console.warn('[securenow] ⚠️ Auto-capture patch failed:', error.message);
184
- console.warn('[securenow] 💡 Body capture disabled. Use manual approach if needed.');
185
- }
186
- } else {
187
- console.log('[securenow] 📝 Automatic body capture: DISABLED (set SECURENOW_CAPTURE_BODY=1 to enable)');
188
- }
189
-
190
- module.exports = {
191
- patchNextRequest,
192
- safeBodyCapture,
193
- redactSensitiveData,
194
- isBodyCaptureEnabled,
195
- };
196
-
197
-
198
-
199
-
1
+ /**
2
+ * SecureNow Next.js Automatic Body Capture
3
+ *
4
+ * This module automatically patches Next.js request handling to capture bodies
5
+ * WITHOUT requiring customers to wrap their handlers or change their code.
6
+ *
7
+ * Usage in instrumentation.ts:
8
+ *
9
+ * import { registerSecureNow } from 'securenow/nextjs';
10
+ * import 'securenow/nextjs-auto-capture'; // Just import this line!
11
+ *
12
+ * export function register() {
13
+ * registerSecureNow();
14
+ * }
15
+ *
16
+ * That's it! Bodies are now captured automatically.
17
+ */
18
+
19
+ const { trace } = require('@opentelemetry/api');
20
+
21
+ // Default sensitive fields to redact
22
+ const DEFAULT_SENSITIVE_FIELDS = [
23
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
24
+ 'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
25
+ 'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
26
+ ];
27
+
28
+ /**
29
+ * Redact sensitive fields from an object
30
+ */
31
+ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
32
+ if (!obj || typeof obj !== 'object') return obj;
33
+
34
+ const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
35
+
36
+ for (const key of Object.keys(redacted)) {
37
+ const lowerKey = key.toLowerCase();
38
+
39
+ if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
40
+ redacted[key] = '[REDACTED]';
41
+ } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
42
+ redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
43
+ }
44
+ }
45
+
46
+ return redacted;
47
+ }
48
+
49
+ /**
50
+ * Safe body capture that doesn't interfere with Next.js
51
+ */
52
+ async function safeBodyCapture(request, span) {
53
+ if (!span) return;
54
+
55
+ try {
56
+ const contentType = request.headers.get('content-type') || '';
57
+ const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
58
+ const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
59
+ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
60
+
61
+ // Only for supported types
62
+ if (!contentType.includes('application/json') &&
63
+ !contentType.includes('application/graphql') &&
64
+ !contentType.includes('application/x-www-form-urlencoded')) {
65
+ return;
66
+ }
67
+
68
+ // Try to read from cache if available (Next.js may have already read it)
69
+ let bodyText;
70
+
71
+ // Attempt 1: Check if body was already cached by Next.js
72
+ if (request._bodyText) {
73
+ bodyText = request._bodyText;
74
+ } else {
75
+ // Attempt 2: Try to clone and read
76
+ try {
77
+ const cloned = request.clone();
78
+ bodyText = await cloned.text();
79
+ // Cache it for Next.js
80
+ request._bodyText = bodyText;
81
+ } catch (e) {
82
+ // If clone fails, body was already consumed - skip silently
83
+ return;
84
+ }
85
+ }
86
+
87
+ if (bodyText.length > maxBodySize) {
88
+ span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
89
+ return;
90
+ }
91
+
92
+ // Parse and redact
93
+ if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
94
+ try {
95
+ const parsed = JSON.parse(bodyText);
96
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
97
+ span.setAttributes({
98
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
99
+ 'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
100
+ 'http.request.body.size': bodyText.length,
101
+ });
102
+ } catch (e) {
103
+ // Parse error - skip
104
+ }
105
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
106
+ try {
107
+ const params = new URLSearchParams(bodyText);
108
+ const parsed = Object.fromEntries(params);
109
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
110
+ span.setAttributes({
111
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
112
+ 'http.request.body.type': 'form',
113
+ 'http.request.body.size': bodyText.length,
114
+ });
115
+ } catch (e) {
116
+ // Parse error - skip
117
+ }
118
+ }
119
+ } catch (error) {
120
+ // Silently fail - never break the request
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Check if body capture is enabled
126
+ */
127
+ function isBodyCaptureEnabled() {
128
+ const enabled = String(process.env.SECURENOW_CAPTURE_BODY) === '1' ||
129
+ String(process.env.SECURENOW_CAPTURE_BODY).toLowerCase() === 'true';
130
+ return enabled;
131
+ }
132
+
133
+ /**
134
+ * Patch Next.js Request to cache body text
135
+ * This allows us to read the body without consuming it
136
+ */
137
+ function patchNextRequest() {
138
+ if (typeof Request === 'undefined') return;
139
+
140
+ const originalText = Request.prototype.text;
141
+ const originalJson = Request.prototype.json;
142
+
143
+ // Patch text() to cache result
144
+ Request.prototype.text = async function() {
145
+ if (this._bodyText !== undefined) {
146
+ return this._bodyText;
147
+ }
148
+ const text = await originalText.call(this);
149
+ this._bodyText = text;
150
+
151
+ // Capture for tracing if enabled
152
+ if (isBodyCaptureEnabled() && ['POST', 'PUT', 'PATCH'].includes(this.method)) {
153
+ const span = trace.getActiveSpan();
154
+ if (span) {
155
+ // Schedule capture after this call (non-blocking)
156
+ setImmediate(() => {
157
+ safeBodyCapture(this, span).catch(() => {});
158
+ });
159
+ }
160
+ }
161
+
162
+ return text;
163
+ };
164
+
165
+ // Patch json() to cache and capture
166
+ Request.prototype.json = async function() {
167
+ // First get text
168
+ const text = await this.text();
169
+ // Then parse
170
+ return JSON.parse(text);
171
+ };
172
+
173
+ console.log('[securenow] ✅ Auto-capture: Patched Next.js Request for automatic body capture');
174
+ }
175
+
176
+ // Auto-patch when module is imported
177
+ if (isBodyCaptureEnabled()) {
178
+ try {
179
+ patchNextRequest();
180
+ console.log('[securenow] 📝 Automatic body capture: ENABLED');
181
+ console.log('[securenow] 💡 No code changes needed - bodies captured automatically!');
182
+ } catch (error) {
183
+ console.warn('[securenow] ⚠️ Auto-capture patch failed:', error.message);
184
+ console.warn('[securenow] 💡 Body capture disabled. Use manual approach if needed.');
185
+ }
186
+ } else {
187
+ console.log('[securenow] 📝 Automatic body capture: DISABLED (set SECURENOW_CAPTURE_BODY=1 to enable)');
188
+ }
189
+
190
+ module.exports = {
191
+ patchNextRequest,
192
+ safeBodyCapture,
193
+ redactSensitiveData,
194
+ isBodyCaptureEnabled,
195
+ };
196
+
197
+
198
+
199
+