securenow 6.0.0 โ 6.0.2
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/docs/CHANGELOG-NEXTJS.md +34 -0
- package/docs/LOGGING-QUICKSTART.md +18 -2
- package/nextjs.js +647 -546
- package/package.json +164 -164
- package/tracing.js +49 -3
package/nextjs.js
CHANGED
|
@@ -1,546 +1,647 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SecureNow Next.js Integration using @vercel/otel
|
|
5
|
-
*
|
|
6
|
-
* Usage in Next.js app:
|
|
7
|
-
*
|
|
8
|
-
* 1. Create instrumentation.ts (or .js) in your project root:
|
|
9
|
-
*
|
|
10
|
-
* import { registerSecureNow } from 'securenow/nextjs';
|
|
11
|
-
* export function register() {
|
|
12
|
-
* registerSecureNow();
|
|
13
|
-
* }
|
|
14
|
-
*
|
|
15
|
-
* 2. Set environment variables:
|
|
16
|
-
* SECURENOW_APPID=my-nextjs-app
|
|
17
|
-
* SECURENOW_INSTANCE=http://your-signoz-host:4318
|
|
18
|
-
*
|
|
19
|
-
* That's it! ๐ No webpack warnings!
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const { v4: uuidv4 } = require('uuid');
|
|
23
|
-
|
|
24
|
-
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
25
|
-
|
|
26
|
-
const parseHeaders = str => {
|
|
27
|
-
const out = {}; if (!str) return out;
|
|
28
|
-
for (const raw of String(str).split(',')) {
|
|
29
|
-
const s = raw.trim(); if (!s) continue;
|
|
30
|
-
const i = s.indexOf('='); if (i === -1) continue;
|
|
31
|
-
out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
let isRegistered = false;
|
|
37
|
-
|
|
38
|
-
// Default sensitive fields to redact from request bodies
|
|
39
|
-
const DEFAULT_SENSITIVE_FIELDS = [
|
|
40
|
-
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
41
|
-
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
42
|
-
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Redact sensitive fields from an object
|
|
47
|
-
*/
|
|
48
|
-
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
49
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
50
|
-
|
|
51
|
-
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
52
|
-
|
|
53
|
-
for (const key in redacted) {
|
|
54
|
-
const lowerKey = key.toLowerCase();
|
|
55
|
-
|
|
56
|
-
// Check if field is sensitive
|
|
57
|
-
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
58
|
-
redacted[key] = '[REDACTED]';
|
|
59
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
60
|
-
// Recursively redact nested objects
|
|
61
|
-
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return redacted;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Redact sensitive data from GraphQL query strings
|
|
70
|
-
*/
|
|
71
|
-
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
72
|
-
if (!query || typeof query !== 'string') return query;
|
|
73
|
-
|
|
74
|
-
let redacted = query;
|
|
75
|
-
|
|
76
|
-
// Redact sensitive fields in GraphQL arguments and variables
|
|
77
|
-
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
78
|
-
sensitiveFields.forEach(field => {
|
|
79
|
-
// Match field: "value" or field: 'value' or field:"value" (with optional spaces)
|
|
80
|
-
const patterns = [
|
|
81
|
-
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
82
|
-
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
patterns.forEach(pattern => {
|
|
86
|
-
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
87
|
-
if (suffix) {
|
|
88
|
-
return `${prefix}[REDACTED]${suffix}`;
|
|
89
|
-
} else {
|
|
90
|
-
return `${prefix}[REDACTED]`;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return redacted;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Parse and capture request body safely
|
|
101
|
-
*/
|
|
102
|
-
async function captureRequestBody(request, maxSize = 10240) {
|
|
103
|
-
try {
|
|
104
|
-
const contentType = request.headers['content-type'] || '';
|
|
105
|
-
let body = '';
|
|
106
|
-
|
|
107
|
-
// Collect body chunks
|
|
108
|
-
const chunks = [];
|
|
109
|
-
let size = 0;
|
|
110
|
-
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
request.on('data', (chunk) => {
|
|
113
|
-
size += chunk.length;
|
|
114
|
-
if (size <= maxSize) {
|
|
115
|
-
chunks.push(chunk);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
request.on('end', () => {
|
|
120
|
-
if (size > maxSize) {
|
|
121
|
-
resolve({
|
|
122
|
-
captured: false,
|
|
123
|
-
reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
|
|
124
|
-
size
|
|
125
|
-
});
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
body = Buffer.concat(chunks).toString('utf8');
|
|
130
|
-
|
|
131
|
-
// Parse based on content type
|
|
132
|
-
if (contentType.includes('application/json')) {
|
|
133
|
-
try {
|
|
134
|
-
const parsed = JSON.parse(body);
|
|
135
|
-
resolve({
|
|
136
|
-
captured: true,
|
|
137
|
-
type: 'json',
|
|
138
|
-
body: parsed,
|
|
139
|
-
size
|
|
140
|
-
});
|
|
141
|
-
} catch (e) {
|
|
142
|
-
resolve({
|
|
143
|
-
captured: true,
|
|
144
|
-
type: 'json',
|
|
145
|
-
body: body.substring(0, 1000),
|
|
146
|
-
parseError: true,
|
|
147
|
-
size
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
} else if (contentType.includes('application/graphql')) {
|
|
151
|
-
// GraphQL queries need redaction too!
|
|
152
|
-
resolve({
|
|
153
|
-
captured: true,
|
|
154
|
-
type: 'graphql',
|
|
155
|
-
body: body, // Will be redacted later
|
|
156
|
-
size
|
|
157
|
-
});
|
|
158
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
159
|
-
// Multipart is NOT captured (files can be huge)
|
|
160
|
-
resolve({
|
|
161
|
-
captured: false,
|
|
162
|
-
type: 'multipart',
|
|
163
|
-
reason: 'Multipart data not captured (file uploads)',
|
|
164
|
-
size
|
|
165
|
-
});
|
|
166
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
167
|
-
try {
|
|
168
|
-
const params = new URLSearchParams(body);
|
|
169
|
-
const parsed = Object.fromEntries(params);
|
|
170
|
-
resolve({
|
|
171
|
-
captured: true,
|
|
172
|
-
type: 'form',
|
|
173
|
-
body: parsed,
|
|
174
|
-
size
|
|
175
|
-
});
|
|
176
|
-
} catch (e) {
|
|
177
|
-
resolve({
|
|
178
|
-
captured: true,
|
|
179
|
-
type: 'form',
|
|
180
|
-
body: body.substring(0, 1000),
|
|
181
|
-
size
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
resolve({
|
|
186
|
-
captured: true,
|
|
187
|
-
type: 'text',
|
|
188
|
-
body: body.substring(0, 1000),
|
|
189
|
-
size
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
request.on('error', () => {
|
|
195
|
-
resolve({ captured: false, reason: 'Stream error' });
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Timeout after 100ms
|
|
199
|
-
setTimeout(() => {
|
|
200
|
-
resolve({ captured: false, reason: 'Timeout' });
|
|
201
|
-
}, 100);
|
|
202
|
-
});
|
|
203
|
-
} catch (error) {
|
|
204
|
-
return { captured: false, reason: error.message };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
210
|
-
* @param {Object} options - Optional configuration
|
|
211
|
-
* @param {string} options.serviceName - Service name (defaults to SECURENOW_APPID or OTEL_SERVICE_NAME)
|
|
212
|
-
* @param {string} options.endpoint - Traces endpoint (defaults to SECURENOW_INSTANCE)
|
|
213
|
-
* @param {boolean} options.noUuid - Don't append UUID to service name
|
|
214
|
-
*/
|
|
215
|
-
function registerSecureNow(options = {}) {
|
|
216
|
-
// Only register once
|
|
217
|
-
if (isRegistered) {
|
|
218
|
-
console.log('[securenow] Already registered, skipping...');
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Skip for Edge runtime
|
|
223
|
-
if (process.env.NEXT_RUNTIME === 'edge') {
|
|
224
|
-
console.log('[securenow] Skipping Edge runtime (Node.js only)');
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Detect environment outside try block for error handling
|
|
229
|
-
const isVercel = !!(env('VERCEL') || env('VERCEL_ENV') || env('VERCEL_URL'));
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
console.log('[securenow] Next.js integration loading (pid=%d)', process.pid);
|
|
233
|
-
|
|
234
|
-
// -------- Configuration --------
|
|
235
|
-
const rawBase = (
|
|
236
|
-
options.serviceName ||
|
|
237
|
-
env('OTEL_SERVICE_NAME') ||
|
|
238
|
-
env('SECURENOW_APPID') ||
|
|
239
|
-
''
|
|
240
|
-
).trim().replace(/^['"]|['"]$/g, '');
|
|
241
|
-
|
|
242
|
-
const baseName = rawBase || null;
|
|
243
|
-
const noUuid = options.noUuid ?? (String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
|
|
244
|
-
|
|
245
|
-
// service.name
|
|
246
|
-
let serviceName;
|
|
247
|
-
if (baseName) {
|
|
248
|
-
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
249
|
-
} else {
|
|
250
|
-
serviceName = `nextjs-app-${uuidv4()}`;
|
|
251
|
-
console.warn('[securenow] โ ๏ธ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
|
|
252
|
-
console.warn('[securenow] ๐ก Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// -------- Endpoint Configuration --------
|
|
256
|
-
const endpointBase = (
|
|
257
|
-
options.endpoint ||
|
|
258
|
-
env('SECURENOW_INSTANCE') ||
|
|
259
|
-
env('OTEL_EXPORTER_OTLP_ENDPOINT') ||
|
|
260
|
-
'https://freetrial.securenow.ai:4318'
|
|
261
|
-
).replace(/\/$/, '');
|
|
262
|
-
|
|
263
|
-
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
process.env.
|
|
268
|
-
process.env.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// ========
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
'http.
|
|
350
|
-
'http.
|
|
351
|
-
'http.
|
|
352
|
-
'http.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
'http.
|
|
357
|
-
'http.
|
|
358
|
-
'http.
|
|
359
|
-
'http.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
'http.
|
|
363
|
-
'http.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'http.
|
|
367
|
-
'http.
|
|
368
|
-
'http.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
'http.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SecureNow Next.js Integration using @vercel/otel
|
|
5
|
+
*
|
|
6
|
+
* Usage in Next.js app:
|
|
7
|
+
*
|
|
8
|
+
* 1. Create instrumentation.ts (or .js) in your project root:
|
|
9
|
+
*
|
|
10
|
+
* import { registerSecureNow } from 'securenow/nextjs';
|
|
11
|
+
* export function register() {
|
|
12
|
+
* registerSecureNow();
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* 2. Set environment variables:
|
|
16
|
+
* SECURENOW_APPID=my-nextjs-app
|
|
17
|
+
* SECURENOW_INSTANCE=http://your-signoz-host:4318
|
|
18
|
+
*
|
|
19
|
+
* That's it! ๐ No webpack warnings!
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { v4: uuidv4 } = require('uuid');
|
|
23
|
+
|
|
24
|
+
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
25
|
+
|
|
26
|
+
const parseHeaders = str => {
|
|
27
|
+
const out = {}; if (!str) return out;
|
|
28
|
+
for (const raw of String(str).split(',')) {
|
|
29
|
+
const s = raw.trim(); if (!s) continue;
|
|
30
|
+
const i = s.indexOf('='); if (i === -1) continue;
|
|
31
|
+
out[s.slice(0, i).trim().toLowerCase()] = s.slice(i + 1).trim();
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let isRegistered = false;
|
|
37
|
+
|
|
38
|
+
// Default sensitive fields to redact from request bodies
|
|
39
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
40
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
41
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
42
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Redact sensitive fields from an object
|
|
47
|
+
*/
|
|
48
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
49
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
50
|
+
|
|
51
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
52
|
+
|
|
53
|
+
for (const key in redacted) {
|
|
54
|
+
const lowerKey = key.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// Check if field is sensitive
|
|
57
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
58
|
+
redacted[key] = '[REDACTED]';
|
|
59
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
60
|
+
// Recursively redact nested objects
|
|
61
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return redacted;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Redact sensitive data from GraphQL query strings
|
|
70
|
+
*/
|
|
71
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
72
|
+
if (!query || typeof query !== 'string') return query;
|
|
73
|
+
|
|
74
|
+
let redacted = query;
|
|
75
|
+
|
|
76
|
+
// Redact sensitive fields in GraphQL arguments and variables
|
|
77
|
+
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
78
|
+
sensitiveFields.forEach(field => {
|
|
79
|
+
// Match field: "value" or field: 'value' or field:"value" (with optional spaces)
|
|
80
|
+
const patterns = [
|
|
81
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
82
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
patterns.forEach(pattern => {
|
|
86
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
87
|
+
if (suffix) {
|
|
88
|
+
return `${prefix}[REDACTED]${suffix}`;
|
|
89
|
+
} else {
|
|
90
|
+
return `${prefix}[REDACTED]`;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return redacted;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse and capture request body safely
|
|
101
|
+
*/
|
|
102
|
+
async function captureRequestBody(request, maxSize = 10240) {
|
|
103
|
+
try {
|
|
104
|
+
const contentType = request.headers['content-type'] || '';
|
|
105
|
+
let body = '';
|
|
106
|
+
|
|
107
|
+
// Collect body chunks
|
|
108
|
+
const chunks = [];
|
|
109
|
+
let size = 0;
|
|
110
|
+
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
request.on('data', (chunk) => {
|
|
113
|
+
size += chunk.length;
|
|
114
|
+
if (size <= maxSize) {
|
|
115
|
+
chunks.push(chunk);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
request.on('end', () => {
|
|
120
|
+
if (size > maxSize) {
|
|
121
|
+
resolve({
|
|
122
|
+
captured: false,
|
|
123
|
+
reason: `Body too large (${size} bytes > ${maxSize} bytes)`,
|
|
124
|
+
size
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
body = Buffer.concat(chunks).toString('utf8');
|
|
130
|
+
|
|
131
|
+
// Parse based on content type
|
|
132
|
+
if (contentType.includes('application/json')) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(body);
|
|
135
|
+
resolve({
|
|
136
|
+
captured: true,
|
|
137
|
+
type: 'json',
|
|
138
|
+
body: parsed,
|
|
139
|
+
size
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
resolve({
|
|
143
|
+
captured: true,
|
|
144
|
+
type: 'json',
|
|
145
|
+
body: body.substring(0, 1000),
|
|
146
|
+
parseError: true,
|
|
147
|
+
size
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else if (contentType.includes('application/graphql')) {
|
|
151
|
+
// GraphQL queries need redaction too!
|
|
152
|
+
resolve({
|
|
153
|
+
captured: true,
|
|
154
|
+
type: 'graphql',
|
|
155
|
+
body: body, // Will be redacted later
|
|
156
|
+
size
|
|
157
|
+
});
|
|
158
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
159
|
+
// Multipart is NOT captured (files can be huge)
|
|
160
|
+
resolve({
|
|
161
|
+
captured: false,
|
|
162
|
+
type: 'multipart',
|
|
163
|
+
reason: 'Multipart data not captured (file uploads)',
|
|
164
|
+
size
|
|
165
|
+
});
|
|
166
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
167
|
+
try {
|
|
168
|
+
const params = new URLSearchParams(body);
|
|
169
|
+
const parsed = Object.fromEntries(params);
|
|
170
|
+
resolve({
|
|
171
|
+
captured: true,
|
|
172
|
+
type: 'form',
|
|
173
|
+
body: parsed,
|
|
174
|
+
size
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {
|
|
177
|
+
resolve({
|
|
178
|
+
captured: true,
|
|
179
|
+
type: 'form',
|
|
180
|
+
body: body.substring(0, 1000),
|
|
181
|
+
size
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
resolve({
|
|
186
|
+
captured: true,
|
|
187
|
+
type: 'text',
|
|
188
|
+
body: body.substring(0, 1000),
|
|
189
|
+
size
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
request.on('error', () => {
|
|
195
|
+
resolve({ captured: false, reason: 'Stream error' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Timeout after 100ms
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
resolve({ captured: false, reason: 'Timeout' });
|
|
201
|
+
}, 100);
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return { captured: false, reason: error.message };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
210
|
+
* @param {Object} options - Optional configuration
|
|
211
|
+
* @param {string} options.serviceName - Service name (defaults to SECURENOW_APPID or OTEL_SERVICE_NAME)
|
|
212
|
+
* @param {string} options.endpoint - Traces endpoint (defaults to SECURENOW_INSTANCE)
|
|
213
|
+
* @param {boolean} options.noUuid - Don't append UUID to service name
|
|
214
|
+
*/
|
|
215
|
+
function registerSecureNow(options = {}) {
|
|
216
|
+
// Only register once
|
|
217
|
+
if (isRegistered) {
|
|
218
|
+
console.log('[securenow] Already registered, skipping...');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Skip for Edge runtime
|
|
223
|
+
if (process.env.NEXT_RUNTIME === 'edge') {
|
|
224
|
+
console.log('[securenow] Skipping Edge runtime (Node.js only)');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Detect environment outside try block for error handling
|
|
229
|
+
const isVercel = !!(env('VERCEL') || env('VERCEL_ENV') || env('VERCEL_URL'));
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
console.log('[securenow] Next.js integration loading (pid=%d)', process.pid);
|
|
233
|
+
|
|
234
|
+
// -------- Configuration --------
|
|
235
|
+
const rawBase = (
|
|
236
|
+
options.serviceName ||
|
|
237
|
+
env('OTEL_SERVICE_NAME') ||
|
|
238
|
+
env('SECURENOW_APPID') ||
|
|
239
|
+
''
|
|
240
|
+
).trim().replace(/^['"]|['"]$/g, '');
|
|
241
|
+
|
|
242
|
+
const baseName = rawBase || null;
|
|
243
|
+
const noUuid = options.noUuid ?? (String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
|
|
244
|
+
|
|
245
|
+
// service.name
|
|
246
|
+
let serviceName;
|
|
247
|
+
if (baseName) {
|
|
248
|
+
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
249
|
+
} else {
|
|
250
|
+
serviceName = `nextjs-app-${uuidv4()}`;
|
|
251
|
+
console.warn('[securenow] โ ๏ธ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
|
|
252
|
+
console.warn('[securenow] ๐ก Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// -------- Endpoint Configuration --------
|
|
256
|
+
const endpointBase = (
|
|
257
|
+
options.endpoint ||
|
|
258
|
+
env('SECURENOW_INSTANCE') ||
|
|
259
|
+
env('OTEL_EXPORTER_OTLP_ENDPOINT') ||
|
|
260
|
+
'https://freetrial.securenow.ai:4318'
|
|
261
|
+
).replace(/\/$/, '');
|
|
262
|
+
|
|
263
|
+
const tracesUrl = env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
264
|
+
const logsUrl = env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`;
|
|
265
|
+
|
|
266
|
+
// Set environment variables for @vercel/otel to pick up
|
|
267
|
+
process.env.OTEL_SERVICE_NAME = serviceName;
|
|
268
|
+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
269
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
270
|
+
|
|
271
|
+
// -------- Logging Configuration --------
|
|
272
|
+
// Opt-in: SECURENOW_LOGGING_ENABLED=1 (or "true").
|
|
273
|
+
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) === '1' ||
|
|
274
|
+
String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() === 'true';
|
|
275
|
+
|
|
276
|
+
console.log('[securenow] ๐ Next.js App โ service.name=%s', serviceName);
|
|
277
|
+
|
|
278
|
+
// -------- Body Capture Configuration --------
|
|
279
|
+
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' ||
|
|
280
|
+
String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true' ||
|
|
281
|
+
options.captureBody === true;
|
|
282
|
+
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
283
|
+
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
284
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
285
|
+
|
|
286
|
+
// -------- Log environment detection --------
|
|
287
|
+
if (!isVercel) {
|
|
288
|
+
console.log('[securenow] ๐ฅ๏ธ Self-hosted environment detected (EC2/PM2) - using vanilla SDK');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// -------- Use different initialization based on environment --------
|
|
292
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
293
|
+
|
|
294
|
+
// Configure HTTP instrumentation with comprehensive header capture
|
|
295
|
+
const httpInstrumentation = new HttpInstrumentation({
|
|
296
|
+
requireParentforOutgoingSpans: false,
|
|
297
|
+
requireParentforIncomingSpans: false,
|
|
298
|
+
ignoreIncomingRequestHook: (request) => {
|
|
299
|
+
// Never ignore - we want to trace all requests
|
|
300
|
+
return false;
|
|
301
|
+
},
|
|
302
|
+
requestHook: (span, request) => {
|
|
303
|
+
// SYNCHRONOUS ONLY - no async operations to avoid timing issues
|
|
304
|
+
try {
|
|
305
|
+
// Capture all headers
|
|
306
|
+
const headers = request.headers || {};
|
|
307
|
+
|
|
308
|
+
// ======== IP ADDRESS CAPTURE ========
|
|
309
|
+
// Try different header sources for IP (priority order)
|
|
310
|
+
const forwardedFor = headers['x-forwarded-for'];
|
|
311
|
+
const realIp = headers['x-real-ip'];
|
|
312
|
+
const cfConnectingIp = headers['cf-connecting-ip']; // Cloudflare
|
|
313
|
+
const clientIp = headers['x-client-ip'];
|
|
314
|
+
const socketIp = request.socket?.remoteAddress;
|
|
315
|
+
|
|
316
|
+
// Primary IP (first in chain is the real client)
|
|
317
|
+
const primaryIp =
|
|
318
|
+
(forwardedFor ? forwardedFor.split(',')[0]?.trim() : null) ||
|
|
319
|
+
realIp ||
|
|
320
|
+
cfConnectingIp ||
|
|
321
|
+
clientIp ||
|
|
322
|
+
socketIp ||
|
|
323
|
+
'unknown';
|
|
324
|
+
|
|
325
|
+
// ======== PROTOCOL & CONNECTION ========
|
|
326
|
+
const scheme = headers['x-forwarded-proto'] ||
|
|
327
|
+
(request.socket?.encrypted ? 'https' : 'http');
|
|
328
|
+
const host = headers['x-forwarded-host'] || headers['host'] || '';
|
|
329
|
+
const port = headers['x-forwarded-port'] || request.socket?.localPort || '';
|
|
330
|
+
|
|
331
|
+
// ======== REQUEST METADATA ========
|
|
332
|
+
const userAgent = headers['user-agent'] || '';
|
|
333
|
+
const referer = headers['referer'] || headers['referrer'] || '';
|
|
334
|
+
const accept = headers['accept'] || '';
|
|
335
|
+
const acceptLanguage = headers['accept-language'] || '';
|
|
336
|
+
const acceptEncoding = headers['accept-encoding'] || '';
|
|
337
|
+
const contentType = headers['content-type'] || '';
|
|
338
|
+
const contentLength = headers['content-length'] || '';
|
|
339
|
+
const origin = headers['origin'] || '';
|
|
340
|
+
|
|
341
|
+
// ======== PROXY & LOAD BALANCER ========
|
|
342
|
+
const originalUri = headers['x-original-uri'] || '';
|
|
343
|
+
const originalMethod = headers['x-original-method'] || '';
|
|
344
|
+
const requestId = headers['x-request-id'] || headers['x-trace-id'] || headers['x-correlation-id'] || '';
|
|
345
|
+
|
|
346
|
+
// ======== SET ALL ATTRIBUTES ========
|
|
347
|
+
const attributes = {
|
|
348
|
+
// IP & Network
|
|
349
|
+
'http.client_ip': primaryIp,
|
|
350
|
+
'http.forwarded_for': forwardedFor || '',
|
|
351
|
+
'http.real_ip': realIp || '',
|
|
352
|
+
'http.socket_ip': socketIp || '',
|
|
353
|
+
|
|
354
|
+
// Protocol & Host
|
|
355
|
+
'http.scheme': scheme,
|
|
356
|
+
'http.host': host,
|
|
357
|
+
'http.port': port.toString(),
|
|
358
|
+
'http.forwarded_proto': headers['x-forwarded-proto'] || '',
|
|
359
|
+
'http.forwarded_host': headers['x-forwarded-host'] || '',
|
|
360
|
+
|
|
361
|
+
// Request Details
|
|
362
|
+
'http.user_agent': userAgent,
|
|
363
|
+
'http.referer': referer,
|
|
364
|
+
'http.origin': origin,
|
|
365
|
+
'http.accept': accept,
|
|
366
|
+
'http.accept_language': acceptLanguage,
|
|
367
|
+
'http.accept_encoding': acceptEncoding,
|
|
368
|
+
'http.content_type': contentType,
|
|
369
|
+
'http.content_length': contentLength,
|
|
370
|
+
|
|
371
|
+
// Proxy & Routing
|
|
372
|
+
'http.original_uri': originalUri,
|
|
373
|
+
'http.original_method': originalMethod,
|
|
374
|
+
'http.request_id': requestId,
|
|
375
|
+
|
|
376
|
+
// Connection Info
|
|
377
|
+
'http.connection': headers['connection'] || '',
|
|
378
|
+
'http.upgrade': headers['upgrade'] || '',
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Set all attributes at once
|
|
382
|
+
span.setAttributes(attributes);
|
|
383
|
+
|
|
384
|
+
// ======== GEOGRAPHIC DATA ========
|
|
385
|
+
// Vercel geo headers
|
|
386
|
+
if (headers['x-vercel-ip-country']) {
|
|
387
|
+
span.setAttributes({
|
|
388
|
+
'http.geo.country': headers['x-vercel-ip-country'],
|
|
389
|
+
'http.geo.region': headers['x-vercel-ip-country-region'] || '',
|
|
390
|
+
'http.geo.city': headers['x-vercel-ip-city'] || '',
|
|
391
|
+
'http.geo.latitude': headers['x-vercel-ip-latitude'] || '',
|
|
392
|
+
'http.geo.longitude': headers['x-vercel-ip-longitude'] || '',
|
|
393
|
+
'http.geo.timezone': headers['x-vercel-ip-timezone'] || '',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Cloudflare geo headers
|
|
398
|
+
if (headers['cf-ipcountry']) {
|
|
399
|
+
span.setAttributes({
|
|
400
|
+
'http.geo.country': headers['cf-ipcountry'],
|
|
401
|
+
'http.geo.cf_ray': headers['cf-ray'] || '',
|
|
402
|
+
'http.geo.cf_visitor': headers['cf-visitor'] || '',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Cloudflare additional headers
|
|
407
|
+
if (headers['cf-connecting-ip']) {
|
|
408
|
+
span.setAttribute('http.cf.connecting_ip', headers['cf-connecting-ip']);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ======== SECURITY HEADERS ========
|
|
412
|
+
if (headers['x-csrf-token']) {
|
|
413
|
+
span.setAttribute('http.security.csrf_token_present', 'true');
|
|
414
|
+
}
|
|
415
|
+
if (headers['authorization']) {
|
|
416
|
+
span.setAttribute('http.security.auth_present', 'true');
|
|
417
|
+
// Never log the actual token!
|
|
418
|
+
}
|
|
419
|
+
if (headers['cookie']) {
|
|
420
|
+
span.setAttribute('http.security.cookies_present', 'true');
|
|
421
|
+
// Never log actual cookies!
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Debug log in development
|
|
425
|
+
if (env('NODE_ENV') === 'development' || env('OTEL_LOG_LEVEL') === 'debug') {
|
|
426
|
+
console.log('[securenow] ๐ก Captured IP: %s (from: %s)',
|
|
427
|
+
primaryIp,
|
|
428
|
+
forwardedFor ? 'x-forwarded-for' : realIp ? 'x-real-ip' : socketIp ? 'socket' : 'unknown'
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// -------- Request Body NOT captured at HTTP instrumentation level --------
|
|
433
|
+
// IMPORTANT: Do NOT attempt to read request.body or listen to 'data' events
|
|
434
|
+
// Next.js manages request streams internally and reading them here causes conflicts
|
|
435
|
+
// Body capture must be done in Next.js middleware using request.clone()
|
|
436
|
+
|
|
437
|
+
} catch (error) {
|
|
438
|
+
// Silently fail to not break the request
|
|
439
|
+
if (env('OTEL_LOG_LEVEL') === 'debug') {
|
|
440
|
+
console.error('[securenow] โ ๏ธ Error in requestHook:', error.message);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
responseHook: (span, response) => {
|
|
445
|
+
try {
|
|
446
|
+
// Capture response metadata
|
|
447
|
+
span.setAttributes({
|
|
448
|
+
'http.status_code': response.statusCode || 0,
|
|
449
|
+
'http.status_message': response.statusMessage || '',
|
|
450
|
+
'http.response.content_type': response.headers?.['content-type'] || '',
|
|
451
|
+
'http.response.content_length': response.headers?.['content-length'] || '',
|
|
452
|
+
});
|
|
453
|
+
} catch (error) {
|
|
454
|
+
// Silently fail
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// -------- Guard against OTLP exporter socket errors --------
|
|
460
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
461
|
+
function _isOtlpTransientError(err) {
|
|
462
|
+
if (!err) return false;
|
|
463
|
+
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
464
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
function _looksLikeOtlpStack(err) {
|
|
468
|
+
const s = err && err.stack;
|
|
469
|
+
if (!s) return false;
|
|
470
|
+
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
471
|
+
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
472
|
+
}
|
|
473
|
+
const _diagDebug = (env('OTEL_LOG_LEVEL') || '').toLowerCase() === 'debug';
|
|
474
|
+
process.on('uncaughtException', (err, origin) => {
|
|
475
|
+
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
476
|
+
if (_diagDebug) {
|
|
477
|
+
console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
throw err;
|
|
482
|
+
});
|
|
483
|
+
process.on('unhandledRejection', (reason) => {
|
|
484
|
+
if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
|
|
485
|
+
if (_diagDebug) {
|
|
486
|
+
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
throw reason;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (isVercel) {
|
|
494
|
+
// -------- Vercel Environment: Use @vercel/otel --------
|
|
495
|
+
const { registerOTel } = require('@vercel/otel');
|
|
496
|
+
|
|
497
|
+
registerOTel({
|
|
498
|
+
serviceName: serviceName,
|
|
499
|
+
attributes: {
|
|
500
|
+
'deployment.environment': env('NODE_ENV') || env('VERCEL_ENV') || 'development',
|
|
501
|
+
'service.version': process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
|
|
502
|
+
'vercel.region': process.env.VERCEL_REGION || undefined,
|
|
503
|
+
},
|
|
504
|
+
instrumentations: [httpInstrumentation],
|
|
505
|
+
instrumentationConfig: {
|
|
506
|
+
fetch: {
|
|
507
|
+
// Propagate context to your backend APIs
|
|
508
|
+
propagateContextUrls: [
|
|
509
|
+
/^https?:\/\/localhost/,
|
|
510
|
+
/^https?:\/\/.*\.vercel\.app/,
|
|
511
|
+
// Add your backend domains here
|
|
512
|
+
],
|
|
513
|
+
// Optionally ignore certain URLs
|
|
514
|
+
ignoreUrls: [
|
|
515
|
+
/_next\/static/,
|
|
516
|
+
/_next\/image/,
|
|
517
|
+
/\.map$/,
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
} else {
|
|
523
|
+
// -------- Self-Hosted (EC2/PM2): Use Vanilla OpenTelemetry SDK --------
|
|
524
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
525
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
526
|
+
const { Resource } = require('@opentelemetry/resources');
|
|
527
|
+
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
528
|
+
|
|
529
|
+
const traceExporter = new OTLPTraceExporter({
|
|
530
|
+
url: tracesUrl,
|
|
531
|
+
headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'))
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const sdk = new NodeSDK({
|
|
535
|
+
serviceName: serviceName,
|
|
536
|
+
traceExporter: traceExporter,
|
|
537
|
+
instrumentations: [httpInstrumentation],
|
|
538
|
+
resource: new Resource({
|
|
539
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
540
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
|
|
541
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
sdk.start();
|
|
546
|
+
console.log('[securenow] ๐ฏ Vanilla SDK initialized for self-hosted environment');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// -------- Logging pipeline (both Vercel and self-hosted) --------
|
|
550
|
+
// Neither @vercel/otel nor NodeSDK 0.47.x wires OTLP logs for us, so we
|
|
551
|
+
// create the LoggerProvider ourselves, register a BatchLogRecordProcessor
|
|
552
|
+
// (addLogRecordProcessor โ the `processors` constructor option was only
|
|
553
|
+
// added in sdk-logs 0.52 and is silently ignored in 0.47), publish it as
|
|
554
|
+
// the global logger provider, and auto-patch console.* to emit records.
|
|
555
|
+
if (loggingEnabled) {
|
|
556
|
+
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
557
|
+
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
558
|
+
const { logs } = require('@opentelemetry/api-logs');
|
|
559
|
+
const { Resource } = require('@opentelemetry/resources');
|
|
560
|
+
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
561
|
+
|
|
562
|
+
const logResource = new Resource({
|
|
563
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
564
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env('NODE_ENV') || env('VERCEL_ENV') || 'production',
|
|
565
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const logExporter = new OTLPLogExporter({
|
|
569
|
+
url: logsUrl,
|
|
570
|
+
headers: parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS')),
|
|
571
|
+
});
|
|
572
|
+
const loggerProvider = new LoggerProvider({ resource: logResource });
|
|
573
|
+
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
|
|
574
|
+
logs.setGlobalLoggerProvider(loggerProvider);
|
|
575
|
+
|
|
576
|
+
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
577
|
+
const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
578
|
+
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
579
|
+
const _emit = (sn, st, args) => {
|
|
580
|
+
try {
|
|
581
|
+
_logger.emit({
|
|
582
|
+
severityNumber: sn,
|
|
583
|
+
severityText: st,
|
|
584
|
+
body: args.map(a => (typeof a === 'object' && a !== null)
|
|
585
|
+
? (() => { try { return JSON.stringify(a); } catch { return String(a); } })()
|
|
586
|
+
: String(a)).join(' '),
|
|
587
|
+
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
588
|
+
});
|
|
589
|
+
} catch (_) {}
|
|
590
|
+
};
|
|
591
|
+
console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
|
|
592
|
+
console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
|
|
593
|
+
console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
|
|
594
|
+
console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
|
|
595
|
+
console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
|
|
596
|
+
|
|
597
|
+
const _shutdownLogs = async () => {
|
|
598
|
+
try { await Promise.resolve(loggerProvider.forceFlush?.()); } catch (_) {}
|
|
599
|
+
try { await Promise.resolve(loggerProvider.shutdown?.()); } catch (_) {}
|
|
600
|
+
};
|
|
601
|
+
process.on('SIGINT', _shutdownLogs);
|
|
602
|
+
process.on('SIGTERM', _shutdownLogs);
|
|
603
|
+
process.on('beforeExit', _shutdownLogs);
|
|
604
|
+
|
|
605
|
+
console.log('[securenow] ๐ Logging: ENABLED โ %s', logsUrl);
|
|
606
|
+
} else {
|
|
607
|
+
console.log('[securenow] ๐ Logging: DISABLED (set SECURENOW_LOGGING_ENABLED=1 to enable)');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
isRegistered = true;
|
|
611
|
+
console.log('[securenow] โ
OpenTelemetry started for Next.js โ %s', tracesUrl);
|
|
612
|
+
console.log('[securenow] ๐ Auto-capturing comprehensive request metadata:');
|
|
613
|
+
console.log('[securenow] โข IP addresses (x-forwarded-for, x-real-ip, socket)');
|
|
614
|
+
console.log('[securenow] โข User-Agent, Referer, Origin, Accept headers');
|
|
615
|
+
console.log('[securenow] โข Protocol, Host, Port (proxy-aware)');
|
|
616
|
+
console.log('[securenow] โข Geographic data (Vercel/Cloudflare)');
|
|
617
|
+
console.log('[securenow] โข Request IDs, CSRF tokens, Auth presence');
|
|
618
|
+
console.log('[securenow] โข Response status, content-type, content-length');
|
|
619
|
+
console.log('[securenow] โ ๏ธ Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
|
|
620
|
+
if (captureBody) {
|
|
621
|
+
console.log('[securenow] ๐ก For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Optional test span
|
|
625
|
+
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
626
|
+
const api = require('@opentelemetry/api');
|
|
627
|
+
const tracer = api.trace.getTracer('securenow-nextjs');
|
|
628
|
+
const span = tracer.startSpan('securenow.nextjs.startup');
|
|
629
|
+
span.setAttribute('next.runtime', process.env.NEXT_RUNTIME || 'nodejs');
|
|
630
|
+
span.end();
|
|
631
|
+
console.log('[securenow] ๐งช Test span created');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error('[securenow] Failed to initialize OpenTelemetry:', error);
|
|
636
|
+
if (isVercel) {
|
|
637
|
+
console.error('[securenow] Make sure you have @vercel/otel installed: npm install @vercel/otel');
|
|
638
|
+
} else {
|
|
639
|
+
console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
module.exports = {
|
|
645
|
+
registerSecureNow,
|
|
646
|
+
};
|
|
647
|
+
|