securenow 7.7.13 → 7.7.15
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/NPM_README.md +98 -140
- package/README.md +46 -32
- package/SKILL-API.md +495 -491
- package/SKILL-CLI.md +9 -9
- package/app-config.js +99 -42
- package/cli/apps.js +589 -597
- package/cli/auth.js +1 -3
- package/cli/config.js +37 -9
- package/cli/credentials.js +1 -1
- package/cli/diagnostics.js +10 -6
- package/cli/init.js +1 -0
- package/free-trial-banner.js +2 -2
- package/mcp/catalog.js +2 -2
- package/nextjs-webpack-config.js +41 -18
- package/nextjs.d.ts +67 -63
- package/nextjs.js +57 -46
- package/nuxt-server-plugin.mjs +6 -11
- package/nuxt.d.ts +42 -38
- package/nuxt.mjs +1 -1
- package/package.json +1 -1
- package/tracing.d.ts +2 -1
- package/tracing.js +31 -47
- package/web-vite.mjs +102 -15
package/nextjs.js
CHANGED
|
@@ -5,19 +5,24 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage in Next.js app:
|
|
7
7
|
*
|
|
8
|
-
* 1. Add securenow to serverExternalPackages
|
|
8
|
+
* 1. Add securenow to serverExternalPackages and standalone output tracing
|
|
9
|
+
* includes in next.config.js:
|
|
9
10
|
*
|
|
10
11
|
* const nextConfig = {
|
|
11
12
|
* serverExternalPackages: ["securenow"],
|
|
13
|
+
* outputFileTracingIncludes: {
|
|
14
|
+
* "/*": ["<securenow package glob>"],
|
|
15
|
+
* },
|
|
12
16
|
* };
|
|
13
17
|
*
|
|
14
18
|
* 2. Create instrumentation.ts (or .js) in your project root:
|
|
15
19
|
*
|
|
16
20
|
* export async function register() {
|
|
17
21
|
* if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
18
|
-
* const securenowNext = await import("securenow/nextjs");
|
|
19
|
-
* securenowNext.registerSecureNow
|
|
20
|
-
*
|
|
22
|
+
* const securenowNext = await import(/* webpackIgnore: true *\/ "securenow/nextjs");
|
|
23
|
+
* const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
|
|
24
|
+
* registerSecureNow({ captureBody: true });
|
|
25
|
+
* await import(/* webpackIgnore: true *\/ "securenow/nextjs-auto-capture");
|
|
21
26
|
* }
|
|
22
27
|
*
|
|
23
28
|
* 3. Run `npx securenow login` and `npx securenow init`.
|
|
@@ -30,8 +35,6 @@ const appConfig = require('./app-config');
|
|
|
30
35
|
const { resolveClientIpWithDetails } = require('./resolve-ip');
|
|
31
36
|
const otelResources = require('@opentelemetry/resources');
|
|
32
37
|
|
|
33
|
-
const env = appConfig.env;
|
|
34
|
-
|
|
35
38
|
let isRegistered = false;
|
|
36
39
|
|
|
37
40
|
function requireRuntimeModule(name) {
|
|
@@ -121,7 +124,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
121
124
|
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
122
125
|
* @param {Object} options - Optional configuration
|
|
123
126
|
* @param {string} options.serviceName - Service name (defaults to .securenow/credentials.json app.key/app.name)
|
|
124
|
-
* @param {string} options.endpoint -
|
|
127
|
+
* @param {string} options.endpoint - Advanced OTLP endpoint override (defaults to the SecureNow ingest gateway)
|
|
125
128
|
* @param {string} options.environment - deployment.environment override (defaults to config.runtime.deploymentEnvironment)
|
|
126
129
|
* @param {boolean} options.noUuid - Don't append UUID to service name
|
|
127
130
|
*/
|
|
@@ -139,24 +142,26 @@ function registerSecureNow(options = {}) {
|
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
// Detect environment outside try block for error handling
|
|
142
|
-
const isVercel = !!(env
|
|
145
|
+
const isVercel = !!(process.env.VERCEL || process.env.VERCEL_ENV || process.env.VERCEL_URL);
|
|
143
146
|
let deploymentEnvironment = appConfig.resolveDeploymentEnvironment();
|
|
144
147
|
|
|
145
148
|
try {
|
|
146
149
|
console.log('[securenow] Next.js integration loading (pid=%d)', process.pid);
|
|
147
150
|
|
|
148
151
|
// -------- Configuration --------
|
|
149
|
-
// Resolution order: explicit options -> .securenow/credentials.json ->
|
|
152
|
+
// Resolution order: explicit options -> .securenow/credentials.json -> package.json#name.
|
|
153
|
+
// Telemetry goes to the stable ingest gateway by default; the API gateway
|
|
154
|
+
// routes by app.key to the dashboard-selected instance.
|
|
150
155
|
const resolvedApp = appConfig.resolveAll();
|
|
151
156
|
|
|
152
157
|
const rawBase = (options.serviceName || resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
153
158
|
const baseName = rawBase || null;
|
|
154
159
|
// Default: auto-disable suffix when logged in (appId is the routing UUID
|
|
155
|
-
// and the dashboard does exact match). opts.noUuid or
|
|
156
|
-
// override.
|
|
160
|
+
// and the dashboard does exact match). opts.noUuid or config.runtime.noUuid
|
|
161
|
+
// can still override.
|
|
157
162
|
const noUuid = appConfig.resolveNoUuid({ noUuid: options.noUuid });
|
|
158
163
|
deploymentEnvironment = appConfig.normalizeDeploymentEnvironment(
|
|
159
|
-
options.environment || resolvedApp.deploymentEnvironment
|
|
164
|
+
options.environment || resolvedApp.deploymentEnvironment
|
|
160
165
|
);
|
|
161
166
|
|
|
162
167
|
// service.name
|
|
@@ -165,7 +170,7 @@ function registerSecureNow(options = {}) {
|
|
|
165
170
|
serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
|
|
166
171
|
} else {
|
|
167
172
|
serviceName = `nextjs-app-${randomUUID()}`;
|
|
168
|
-
console.warn('[securenow]
|
|
173
|
+
console.warn('[securenow] No app identity resolved. Using fallback: %s', serviceName);
|
|
169
174
|
console.warn('[securenow] Run `npx securenow login` and `npx securenow init` to write .securenow/credentials.json');
|
|
170
175
|
}
|
|
171
176
|
|
|
@@ -175,24 +180,30 @@ function registerSecureNow(options = {}) {
|
|
|
175
180
|
const tracesUrl = resolvedEndpoints.tracesUrl;
|
|
176
181
|
const logsUrl = resolvedEndpoints.logsUrl;
|
|
177
182
|
const headers = resolvedEndpoints.headers;
|
|
183
|
+
const otelLogLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
184
|
+
const isDevelopmentRuntime = process.env.NODE_ENV === 'development';
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
186
|
+
// @vercel/otel still reads OTel process variables internally. These are
|
|
187
|
+
// derived from credentials JSON; customers do not set them.
|
|
188
|
+
if (isVercel) {
|
|
189
|
+
if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
|
|
190
|
+
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
191
|
+
if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
192
|
+
}
|
|
182
193
|
|
|
183
194
|
console.log('[securenow] Next.js App -> service.name=%s', serviceName);
|
|
184
195
|
console.log('[securenow] Environment: %s', deploymentEnvironment);
|
|
185
196
|
|
|
186
197
|
// -------- Body Capture Configuration --------
|
|
187
198
|
// Opt-out default: set config.capture.body=false (or options.captureBody=false) to disable.
|
|
188
|
-
const captureBody = options.captureBody ??
|
|
189
|
-
const maxBodySize =
|
|
190
|
-
const customSensitiveFields = (
|
|
199
|
+
const captureBody = options.captureBody ?? appConfig.boolConfig('capture.body', true);
|
|
200
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
201
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
191
202
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
192
203
|
|
|
193
204
|
// -------- Log environment detection --------
|
|
194
205
|
if (!isVercel) {
|
|
195
|
-
console.log('[securenow]
|
|
206
|
+
console.log('[securenow] Self-hosted environment detected (EC2/PM2) - using vanilla SDK');
|
|
196
207
|
}
|
|
197
208
|
|
|
198
209
|
// -------- Use different initialization based on environment --------
|
|
@@ -326,8 +337,8 @@ function registerSecureNow(options = {}) {
|
|
|
326
337
|
}
|
|
327
338
|
|
|
328
339
|
// Debug log in development
|
|
329
|
-
if (
|
|
330
|
-
console.log('[securenow]
|
|
340
|
+
if (isDevelopmentRuntime || otelLogLevel === 'debug') {
|
|
341
|
+
console.log('[securenow] Captured IP: %s (from: %s)',
|
|
331
342
|
primaryIp,
|
|
332
343
|
ipDetails.source || 'unknown'
|
|
333
344
|
);
|
|
@@ -340,8 +351,8 @@ function registerSecureNow(options = {}) {
|
|
|
340
351
|
|
|
341
352
|
} catch (error) {
|
|
342
353
|
// Silently fail to not break the request
|
|
343
|
-
if (
|
|
344
|
-
console.error('[securenow]
|
|
354
|
+
if (otelLogLevel === 'debug') {
|
|
355
|
+
console.error('[securenow] Error in requestHook:', error.message);
|
|
345
356
|
}
|
|
346
357
|
}
|
|
347
358
|
},
|
|
@@ -378,7 +389,7 @@ function registerSecureNow(options = {}) {
|
|
|
378
389
|
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
379
390
|
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
380
391
|
}
|
|
381
|
-
const _diagDebug =
|
|
392
|
+
const _diagDebug = otelLogLevel === 'debug';
|
|
382
393
|
process.on('uncaughtException', (err, origin) => {
|
|
383
394
|
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
384
395
|
if (_diagDebug) {
|
|
@@ -450,11 +461,11 @@ function registerSecureNow(options = {}) {
|
|
|
450
461
|
});
|
|
451
462
|
|
|
452
463
|
sdk.start();
|
|
453
|
-
console.log('[securenow]
|
|
464
|
+
console.log('[securenow] Vanilla SDK initialized for self-hosted environment');
|
|
454
465
|
|
|
455
466
|
// -------- Logging (self-hosted only) --------
|
|
456
467
|
// Opt-out default: set config.logging.enabled=false to disable.
|
|
457
|
-
const loggingEnabled =
|
|
468
|
+
const loggingEnabled = appConfig.boolConfig('logging.enabled', true);
|
|
458
469
|
if (loggingEnabled) {
|
|
459
470
|
try {
|
|
460
471
|
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
@@ -506,7 +517,7 @@ function registerSecureNow(options = {}) {
|
|
|
506
517
|
_emitLog(SeverityNumber.ERROR, 'ERROR', args);
|
|
507
518
|
};
|
|
508
519
|
|
|
509
|
-
console.log('[securenow]
|
|
520
|
+
console.log('[securenow] Logging: ENABLED -> %s', logsUrl);
|
|
510
521
|
|
|
511
522
|
// Auto-log every incoming HTTP request/response
|
|
512
523
|
try {
|
|
@@ -554,51 +565,51 @@ function registerSecureNow(options = {}) {
|
|
|
554
565
|
}
|
|
555
566
|
return originalEmit.apply(this, arguments);
|
|
556
567
|
};
|
|
557
|
-
console.log('[securenow]
|
|
568
|
+
console.log('[securenow] HTTP request logging: ENABLED');
|
|
558
569
|
} catch (_) {}
|
|
559
570
|
|
|
560
571
|
// Graceful shutdown for logs
|
|
561
572
|
process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
|
|
562
573
|
process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
|
|
563
574
|
} catch (e) {
|
|
564
|
-
console.warn('[securenow]
|
|
575
|
+
console.warn('[securenow] Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
|
|
565
576
|
}
|
|
566
577
|
} else {
|
|
567
|
-
console.log('[securenow]
|
|
578
|
+
console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
|
|
568
579
|
}
|
|
569
580
|
}
|
|
570
581
|
|
|
571
582
|
isRegistered = true;
|
|
572
583
|
|
|
573
|
-
// Free trial banner (optional
|
|
584
|
+
// Free trial banner (optional - may not be bundled in standalone builds)
|
|
574
585
|
try {
|
|
575
586
|
const { isFreeTrial, patchHttpForBanner } = requireRuntimeModule('./free-trial-banner');
|
|
576
|
-
if (isFreeTrial(endpointBase) &&
|
|
587
|
+
if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
|
|
577
588
|
patchHttpForBanner();
|
|
578
589
|
}
|
|
579
590
|
} catch (_) {}
|
|
580
591
|
|
|
581
|
-
console.log('[securenow]
|
|
582
|
-
console.log('[securenow]
|
|
583
|
-
console.log('[securenow]
|
|
584
|
-
console.log('[securenow]
|
|
585
|
-
console.log('[securenow]
|
|
586
|
-
console.log('[securenow]
|
|
587
|
-
console.log('[securenow]
|
|
588
|
-
console.log('[securenow]
|
|
589
|
-
console.log('[securenow]
|
|
592
|
+
console.log('[securenow] OpenTelemetry started for Next.js -> %s', tracesUrl);
|
|
593
|
+
console.log('[securenow] Auto-capturing comprehensive request metadata:');
|
|
594
|
+
console.log('[securenow] - IP addresses (x-forwarded-for, x-real-ip, socket)');
|
|
595
|
+
console.log('[securenow] - User-Agent, Referer, Origin, Accept headers');
|
|
596
|
+
console.log('[securenow] - Protocol, Host, Port (proxy-aware)');
|
|
597
|
+
console.log('[securenow] - Geographic data (Vercel/Cloudflare)');
|
|
598
|
+
console.log('[securenow] - Request IDs, CSRF tokens, Auth presence');
|
|
599
|
+
console.log('[securenow] - Response status, content-type, content-length');
|
|
600
|
+
console.log('[securenow] Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
|
|
590
601
|
if (captureBody) {
|
|
591
|
-
console.log('[securenow]
|
|
602
|
+
console.log('[securenow] For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
|
|
592
603
|
}
|
|
593
604
|
|
|
594
605
|
// Optional test span
|
|
595
|
-
if (
|
|
606
|
+
if (appConfig.boolConfig('runtime.testSpan', false)) {
|
|
596
607
|
const api = require('@opentelemetry/api');
|
|
597
608
|
const tracer = api.trace.getTracer('securenow-nextjs');
|
|
598
609
|
const span = tracer.startSpan('securenow.nextjs.startup');
|
|
599
610
|
span.setAttribute('next.runtime', process.env.NEXT_RUNTIME || 'nodejs');
|
|
600
611
|
span.end();
|
|
601
|
-
console.log('[securenow]
|
|
612
|
+
console.log('[securenow] Test span created');
|
|
602
613
|
}
|
|
603
614
|
|
|
604
615
|
} catch (error) {
|
|
@@ -610,7 +621,7 @@ function registerSecureNow(options = {}) {
|
|
|
610
621
|
}
|
|
611
622
|
}
|
|
612
623
|
|
|
613
|
-
// Firewall
|
|
624
|
+
// Firewall - runs independently from OTel so it works even if tracing fails.
|
|
614
625
|
// Key and environment come from .securenow/credentials.json (written by
|
|
615
626
|
// login/init or credentials runtime), so no .env entry is needed.
|
|
616
627
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -26,8 +26,6 @@ const { resolveClientIpWithDetails } = nodeRequire('./resolve-ip');
|
|
|
26
26
|
|
|
27
27
|
// ── Helpers ──
|
|
28
28
|
|
|
29
|
-
const env = appConfig.env;
|
|
30
|
-
|
|
31
29
|
function createResource(attributes) {
|
|
32
30
|
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
33
31
|
return otelResources.resourceFromAttributes(attributes);
|
|
@@ -74,7 +72,7 @@ function getRuntimeOptions() {
|
|
|
74
72
|
export default defineNitroPlugin(async (nitroApp) => {
|
|
75
73
|
const opts = getRuntimeOptions();
|
|
76
74
|
|
|
77
|
-
// Resolution order: opts -> .securenow/credentials.json ->
|
|
75
|
+
// Resolution order: opts -> .securenow/credentials.json -> package.json#name
|
|
78
76
|
const resolvedApp = appConfig.resolveAll();
|
|
79
77
|
|
|
80
78
|
// ── Naming ──
|
|
@@ -125,12 +123,9 @@ export default defineNitroPlugin(async (nitroApp) => {
|
|
|
125
123
|
// Opt-out default: set config.capture.body=false (or opts.captureBody=false) to disable.
|
|
126
124
|
const captureBody =
|
|
127
125
|
opts.captureBody ??
|
|
128
|
-
|
|
129
|
-
const maxBodySize =
|
|
130
|
-
const customSensitiveFields = (
|
|
131
|
-
.split(',')
|
|
132
|
-
.map((s) => s.trim())
|
|
133
|
-
.filter(Boolean);
|
|
126
|
+
appConfig.boolConfig('capture.body', true);
|
|
127
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
128
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
134
129
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
135
130
|
|
|
136
131
|
// ── HTTP instrumentation ──
|
|
@@ -238,7 +233,7 @@ export default defineNitroPlugin(async (nitroApp) => {
|
|
|
238
233
|
// Opt-out default: set config.logging.enabled=false (or opts.logging=false) to disable.
|
|
239
234
|
const loggingEnabled =
|
|
240
235
|
opts.logging ??
|
|
241
|
-
|
|
236
|
+
appConfig.boolConfig('logging.enabled', true);
|
|
242
237
|
|
|
243
238
|
let loggerProvider = null;
|
|
244
239
|
|
|
@@ -314,7 +309,7 @@ export default defineNitroPlugin(async (nitroApp) => {
|
|
|
314
309
|
// ── Free trial banner ──
|
|
315
310
|
try {
|
|
316
311
|
const { isFreeTrial, patchHttpForBanner } = await import('./free-trial-banner.js');
|
|
317
|
-
if (isFreeTrial(endpointBase) &&
|
|
312
|
+
if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
|
|
318
313
|
patchHttpForBanner();
|
|
319
314
|
}
|
|
320
315
|
} catch {
|
package/nuxt.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecureNow Nuxt 3 Module TypeScript Declarations
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface SecureNowNuxtOptions {
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Nuxt 3 Module TypeScript Declarations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface SecureNowNuxtOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Service name for OpenTelemetry traces.
|
|
8
8
|
* @default .securenow/credentials.json app.key/app.name
|
|
@@ -11,7 +11,11 @@ export interface SecureNowNuxtOptions {
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* OTLP endpoint base URL.
|
|
14
|
-
* @default
|
|
14
|
+
* @default https://ingest.securenow.ai
|
|
15
|
+
*
|
|
16
|
+
* Advanced OTLP endpoint override. Normal SecureNow apps should leave this
|
|
17
|
+
* unset so the ingest gateway can route by app.key to the dashboard-selected
|
|
18
|
+
* instance.
|
|
15
19
|
*/
|
|
16
20
|
endpoint?: string;
|
|
17
21
|
|
|
@@ -20,14 +24,14 @@ export interface SecureNowNuxtOptions {
|
|
|
20
24
|
* @default .securenow/credentials.json config.runtime.deploymentEnvironment
|
|
21
25
|
*/
|
|
22
26
|
environment?: string;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Don't append UUID to service name (useful when running a single instance).
|
|
26
|
-
* @default false
|
|
27
|
-
*/
|
|
28
|
-
noUuid?: boolean;
|
|
29
|
-
|
|
30
|
-
/**
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Don't append UUID to service name (useful when running a single instance).
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
noUuid?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
31
35
|
* Capture request bodies (POST/PUT/PATCH) on traced spans.
|
|
32
36
|
* Sensitive fields are automatically redacted.
|
|
33
37
|
* @default true from .securenow/credentials.json secure defaults
|
|
@@ -40,27 +44,27 @@ export interface SecureNowNuxtOptions {
|
|
|
40
44
|
*/
|
|
41
45
|
logging?: boolean;
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
declare module 'nuxt/schema' {
|
|
45
|
-
interface NuxtConfig {
|
|
46
|
-
securenow?: SecureNowNuxtOptions;
|
|
47
|
-
}
|
|
48
|
-
interface NuxtOptions {
|
|
49
|
-
securenow?: SecureNowNuxtOptions;
|
|
50
|
-
}
|
|
51
|
-
interface RuntimeConfig {
|
|
52
|
-
securenow?: SecureNowNuxtOptions;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
declare module '@nuxt/schema' {
|
|
57
|
-
interface NuxtConfig {
|
|
58
|
-
securenow?: SecureNowNuxtOptions;
|
|
59
|
-
}
|
|
60
|
-
interface NuxtOptions {
|
|
61
|
-
securenow?: SecureNowNuxtOptions;
|
|
62
|
-
}
|
|
63
|
-
interface RuntimeConfig {
|
|
64
|
-
securenow?: SecureNowNuxtOptions;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
47
|
+
|
|
48
|
+
declare module 'nuxt/schema' {
|
|
49
|
+
interface NuxtConfig {
|
|
50
|
+
securenow?: SecureNowNuxtOptions;
|
|
51
|
+
}
|
|
52
|
+
interface NuxtOptions {
|
|
53
|
+
securenow?: SecureNowNuxtOptions;
|
|
54
|
+
}
|
|
55
|
+
interface RuntimeConfig {
|
|
56
|
+
securenow?: SecureNowNuxtOptions;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare module '@nuxt/schema' {
|
|
61
|
+
interface NuxtConfig {
|
|
62
|
+
securenow?: SecureNowNuxtOptions;
|
|
63
|
+
}
|
|
64
|
+
interface NuxtOptions {
|
|
65
|
+
securenow?: SecureNowNuxtOptions;
|
|
66
|
+
}
|
|
67
|
+
interface RuntimeConfig {
|
|
68
|
+
securenow?: SecureNowNuxtOptions;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/nuxt.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* });
|
|
12
12
|
*
|
|
13
13
|
* Environment variables (same as all SecureNow integrations):
|
|
14
|
-
*
|
|
14
|
+
* Configuration is read from .securenow/credentials.json.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { defineNuxtModule, createResolver, addServerPlugin } from '@nuxt/kit';
|
package/package.json
CHANGED
package/tracing.d.ts
CHANGED
|
@@ -173,7 +173,8 @@ export const loggerProvider: LoggerProvider | null;
|
|
|
173
173
|
* generated JSON to .securenow/credentials.json in the running app.
|
|
174
174
|
*
|
|
175
175
|
* Main config fields:
|
|
176
|
-
* - app.key / app.name
|
|
176
|
+
* - app.key / app.name
|
|
177
|
+
* - config.otel.endpoint only for advanced/self-hosted collector overrides
|
|
177
178
|
* - apiKey
|
|
178
179
|
* - config.runtime.deploymentEnvironment
|
|
179
180
|
* - config.logging.enabled
|
package/tracing.js
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Preload with: node --require securenow/register app.js
|
|
5
5
|
*
|
|
6
6
|
* Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
|
|
7
|
-
* auto-registered via module.register()
|
|
7
|
+
* auto-registered via module.register() — no --import flag needed.
|
|
8
8
|
* On Node 18 with "type": "module", add the hook manually:
|
|
9
9
|
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* dashboard does exact match),
|
|
16
|
-
* 0 pre-login (use suffix to
|
|
17
|
-
* distinguish PM2 cluster workers).
|
|
18
|
-
* SECURENOW_INSTANCE=http://host:4318 # OTLP/HTTP base (default https://freetrial.securenow.ai:4318)
|
|
19
|
-
* OTEL_EXPORTER_OTLP_ENDPOINT=... # alternative base
|
|
20
|
-
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
|
|
21
|
-
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
22
|
-
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
23
|
-
* SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
|
|
24
|
-
* OTEL_LOG_LEVEL=error|warn|info|debug|none
|
|
25
|
-
* SECURENOW_TEST_SPAN=1
|
|
26
|
-
*
|
|
27
|
-
* Safety:
|
|
28
|
-
* SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
|
|
11
|
+
* Config:
|
|
12
|
+
* Runtime config is read from .securenow/credentials.json.
|
|
13
|
+
* Run `npx securenow login` and `npx securenow init` to create it.
|
|
14
|
+
* Production should mount/copy tokenless runtime credentials to the same path.
|
|
29
15
|
*/
|
|
30
16
|
|
|
31
17
|
const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
|
|
@@ -40,8 +26,6 @@ const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongo
|
|
|
40
26
|
const { randomUUID } = require('crypto');
|
|
41
27
|
const appConfig = require('./app-config');
|
|
42
28
|
|
|
43
|
-
const env = appConfig.env;
|
|
44
|
-
|
|
45
29
|
function createResource(attributes) {
|
|
46
30
|
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
47
31
|
return otelResources.resourceFromAttributes(attributes);
|
|
@@ -265,7 +249,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
265
249
|
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
266
250
|
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
267
251
|
if (!hasCliHook && !hasModuleRegister) {
|
|
268
|
-
console.warn('[securenow]
|
|
252
|
+
console.warn('[securenow] âš ï¸ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
269
253
|
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
270
254
|
}
|
|
271
255
|
}
|
|
@@ -274,7 +258,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
274
258
|
})();
|
|
275
259
|
|
|
276
260
|
// -------- diagnostics --------
|
|
277
|
-
const diagLevel = ((
|
|
261
|
+
const diagLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
278
262
|
(() => {
|
|
279
263
|
const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
|
|
280
264
|
diagLevel === 'info' ? DiagLogLevel.INFO :
|
|
@@ -285,7 +269,7 @@ const diagLevel = ((process.env.OTEL_LOG_LEVEL != null ? process.env.OTEL_LOG_LE
|
|
|
285
269
|
})();
|
|
286
270
|
|
|
287
271
|
// -------- endpoints & app resolution --------
|
|
288
|
-
// Resolution order for endpoint/appId/apiKey: .securenow/credentials.json ->
|
|
272
|
+
// Resolution order for endpoint/appId/apiKey: .securenow/credentials.json -> package.json#name -> defaults.
|
|
289
273
|
const resolvedApp = appConfig.resolveAll();
|
|
290
274
|
const resolvedEndpoints = appConfig.resolveEndpoints();
|
|
291
275
|
|
|
@@ -301,10 +285,10 @@ const headers = resolvedEndpoints.headers;
|
|
|
301
285
|
const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
302
286
|
const baseName = rawBase || null;
|
|
303
287
|
// Auto-disables the per-worker suffix when we resolved a routing UUID from
|
|
304
|
-
// credentials
|
|
288
|
+
// credentials — the dashboard does exact-match IN on service.name, so any
|
|
305
289
|
// suffix breaks routing. config.runtime.noUuid can override.
|
|
306
290
|
const noUuid = appConfig.resolveNoUuid();
|
|
307
|
-
const strict =
|
|
291
|
+
const strict = appConfig.boolConfig('runtime.strict', false);
|
|
308
292
|
const inPm2Cluster = !!(process.env.NODE_APP_INSTANCE || process.env.pm_id);
|
|
309
293
|
|
|
310
294
|
// Fail fast in cluster if base is missing (no more "free" names)
|
|
@@ -328,7 +312,7 @@ const instancePrefix = baseName || 'securenow';
|
|
|
328
312
|
const serviceInstanceId = `${instancePrefix}-${randomUUID()}`;
|
|
329
313
|
|
|
330
314
|
// Loud line per worker to prove what was used
|
|
331
|
-
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s
|
|
315
|
+
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=%s instance.id=%s',
|
|
332
316
|
process.pid,
|
|
333
317
|
JSON.stringify(baseName),
|
|
334
318
|
JSON.stringify(endpointBase),
|
|
@@ -339,18 +323,18 @@ console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=
|
|
|
339
323
|
|
|
340
324
|
// -------- instrumentations --------
|
|
341
325
|
const disabledMap = {};
|
|
342
|
-
for (const n of (
|
|
326
|
+
for (const n of appConfig.listConfig('otel.disableInstrumentations')) {
|
|
343
327
|
disabledMap[n] = { enabled: false };
|
|
344
328
|
}
|
|
345
329
|
|
|
346
330
|
// -------- Body Capture Configuration --------
|
|
347
331
|
// Opt-out defaults: set config.capture.body=false to disable.
|
|
348
|
-
const captureBody =
|
|
349
|
-
const maxBodySize =
|
|
350
|
-
const customSensitiveFields = (
|
|
332
|
+
const captureBody = appConfig.boolConfig('capture.body', true);
|
|
333
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
334
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
351
335
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
352
336
|
|
|
353
|
-
const captureMultipart =
|
|
337
|
+
const captureMultipart = appConfig.boolConfig('capture.multipart', true);
|
|
354
338
|
|
|
355
339
|
const BODY_CAPTURE_PATCH = Symbol.for('securenow.bodyCapture.emitPatch');
|
|
356
340
|
|
|
@@ -370,7 +354,7 @@ function installRequestBodyObserver(span, request, contentType) {
|
|
|
370
354
|
if (isMultipartBody && !captureMultipart) {
|
|
371
355
|
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
372
356
|
span.setAttribute('http.request.body.type', 'multipart');
|
|
373
|
-
span.setAttribute('http.request.body.note', 'Multipart capture disabled
|
|
357
|
+
span.setAttribute('http.request.body.note', 'Multipart capture disabled by config.capture.multipart=false');
|
|
374
358
|
return;
|
|
375
359
|
}
|
|
376
360
|
|
|
@@ -537,8 +521,8 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
537
521
|
});
|
|
538
522
|
|
|
539
523
|
// -------- Logging Configuration --------
|
|
540
|
-
// Opt-out default: set =
|
|
541
|
-
const loggingEnabled =
|
|
524
|
+
// Opt-out default: set config.logging.enabled=false to disable.
|
|
525
|
+
const loggingEnabled = appConfig.boolConfig('logging.enabled', true);
|
|
542
526
|
|
|
543
527
|
// Create shared resource for both traces and logs
|
|
544
528
|
const sharedResource = createResource({
|
|
@@ -615,9 +599,9 @@ process.on('uncaughtException', (err, origin) => {
|
|
|
615
599
|
if (diagLevel === 'debug') {
|
|
616
600
|
console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
|
|
617
601
|
}
|
|
618
|
-
return; // swallow
|
|
602
|
+
return; // swallow — do not crash
|
|
619
603
|
}
|
|
620
|
-
// Not ours
|
|
604
|
+
// Not ours — re-throw so the default handler (or the app's own handler) fires
|
|
621
605
|
throw err;
|
|
622
606
|
});
|
|
623
607
|
process.on('unhandledRejection', (reason) => {
|
|
@@ -627,7 +611,7 @@ process.on('unhandledRejection', (reason) => {
|
|
|
627
611
|
}
|
|
628
612
|
return; // swallow
|
|
629
613
|
}
|
|
630
|
-
// Not ours
|
|
614
|
+
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
631
615
|
throw reason;
|
|
632
616
|
});
|
|
633
617
|
|
|
@@ -651,19 +635,19 @@ const sdk = new NodeSDK({
|
|
|
651
635
|
(async () => {
|
|
652
636
|
try {
|
|
653
637
|
await Promise.resolve(sdk.start?.());
|
|
654
|
-
console.log('[securenow] OTel SDK started
|
|
638
|
+
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
655
639
|
if (loggingEnabled) {
|
|
656
|
-
console.log('[securenow]
|
|
640
|
+
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
657
641
|
} else {
|
|
658
642
|
console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
|
|
659
643
|
}
|
|
660
644
|
if (captureBody) {
|
|
661
|
-
console.log('[securenow]
|
|
645
|
+
console.log('[securenow] 📠Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
662
646
|
}
|
|
663
647
|
if (captureMultipart) {
|
|
664
|
-
console.log('[securenow]
|
|
648
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
665
649
|
}
|
|
666
|
-
if (
|
|
650
|
+
if (appConfig.boolConfig('runtime.testSpan', false)) {
|
|
667
651
|
const api = require('@opentelemetry/api');
|
|
668
652
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
669
653
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
@@ -671,13 +655,13 @@ const sdk = new NodeSDK({
|
|
|
671
655
|
|
|
672
656
|
// Free trial banner
|
|
673
657
|
const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
|
|
674
|
-
if (isFreeTrial(endpointBase) &&
|
|
658
|
+
if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
|
|
675
659
|
patchHttpForBanner();
|
|
676
660
|
}
|
|
677
661
|
|
|
678
|
-
// Firewall
|
|
662
|
+
// Firewall — auto-activates only when a real snk_live_ key is resolvable.
|
|
679
663
|
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
680
|
-
// only an app-routing UUID (or nothing at all)
|
|
664
|
+
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
681
665
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
682
666
|
if (firewallOptions.apiKey && firewallOptions.enabled) {
|
|
683
667
|
require('./firewall').init({
|