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/nextjs.js CHANGED
@@ -5,19 +5,24 @@
5
5
  *
6
6
  * Usage in Next.js app:
7
7
  *
8
- * 1. Add securenow to serverExternalPackages in next.config.js:
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({ captureBody: true });
20
- * await import("securenow/nextjs-auto-capture");
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 - Traces endpoint (defaults to .securenow/credentials.json app.instance)
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('VERCEL') || env('VERCEL_ENV') || env('VERCEL_URL'));
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 -> legacy env fallback -> package.json#name
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 SECURENOW_NO_UUID
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 || env('VERCEL_ENV')
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] ⚠️ No app identity resolved. Using fallback: %s', serviceName);
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
- if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
180
- if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
181
- if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
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 ?? !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
189
- const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
190
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
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] 🖥️ Self-hosted environment detected (EC2/PM2) - using vanilla SDK');
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 (env('NODE_ENV') === 'development' || env('OTEL_LOG_LEVEL') === 'debug') {
330
- console.log('[securenow] 📡 Captured IP: %s (from: %s)',
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 (env('OTEL_LOG_LEVEL') === 'debug') {
344
- console.error('[securenow] ⚠️ Error in requestHook:', error.message);
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 = (env('OTEL_LOG_LEVEL') || '').toLowerCase() === 'debug';
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] 🎯 Vanilla SDK initialized for self-hosted environment');
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 = !/^(0|false)$/i.test(String(env('SECURENOW_LOGGING_ENABLED') ?? ''));
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] 📋 Logging: ENABLED %s', logsUrl);
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] 📋 HTTP request logging: ENABLED');
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] ⚠️ Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
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] 📋 Logging: DISABLED (SECURENOW_LOGGING_ENABLED=0)');
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 may not be bundled in standalone builds)
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) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
587
+ if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
577
588
  patchHttpForBanner();
578
589
  }
579
590
  } catch (_) {}
580
591
 
581
- console.log('[securenow] OpenTelemetry started for Next.js %s', tracesUrl);
582
- console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
583
- console.log('[securenow] IP addresses (x-forwarded-for, x-real-ip, socket)');
584
- console.log('[securenow] User-Agent, Referer, Origin, Accept headers');
585
- console.log('[securenow] Protocol, Host, Port (proxy-aware)');
586
- console.log('[securenow] Geographic data (Vercel/Cloudflare)');
587
- console.log('[securenow] Request IDs, CSRF tokens, Auth presence');
588
- console.log('[securenow] Response status, content-type, content-length');
589
- console.log('[securenow] ⚠️ Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
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] 💡 For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
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 (String(env('SECURENOW_TEST_SPAN')) === '1') {
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] 🧪 Test span created');
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 runs independently from OTel so it works even if tracing fails.
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();
@@ -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 -> legacy env fallback -> package.json#name
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
- !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
129
- const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
130
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '')
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
- !/^(0|false)$/i.test(String(env('SECURENOW_LOGGING_ENABLED') ?? ''));
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) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
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 .securenow/credentials.json app.instance, or https://freetrial.securenow.ai:4318
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
- * SECURENOW_APPID, SECURENOW_INSTANCE, SECURENOW_LOGGING_ENABLED, etc.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.13",
3
+ "version": "7.7.15",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
package/tracing.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 / app.instance
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() no --import flag needed.
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
- * Env:
12
- * SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
13
- * SECURENOW_NO_UUID=1|0 # override. Default: auto 1 when
14
- * logged in (appId is routing UUID,
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] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
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 = ((process.env.OTEL_LOG_LEVEL != null ? process.env.OTEL_LOG_LEVEL : env('OTEL_LOG_LEVEL')) || '').toLowerCase();
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 -> legacy env fallback -> package.json#name -> defaults.
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 the dashboard does exact-match IN on service.name, so any
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 = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
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 service.name=%s instance.id=%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 (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map(s => s.trim()).filter(Boolean)) {
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 = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
349
- const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
350
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
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 = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
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 (SECURENOW_CAPTURE_MULTIPART=0)');
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 =0 or =false to disable.
541
- const loggingEnabled = !/^(0|false)$/i.test(String(env('SECURENOW_LOGGING_ENABLED') ?? ''));
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 do not crash
602
+ return; // swallow — do not crash
619
603
  }
620
- // Not ours re-throw so the default handler (or the app's own handler) fires
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 re-throw as unhandled so Node's default behaviour applies
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 %s', tracesUrl);
638
+ console.log('[securenow] OTel SDK started → %s', tracesUrl);
655
639
  if (loggingEnabled) {
656
- console.log('[securenow] 📋 Logging: ENABLED %s', logsUrl);
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] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
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] 📎 Multipart body capture: ENABLED (streaming file content not buffered)');
648
+ console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
665
649
  }
666
- if (String(env('SECURENOW_TEST_SPAN')) === '1') {
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) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
658
+ if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
675
659
  patchHttpForBanner();
676
660
  }
677
661
 
678
- // Firewall auto-activates only when a real snk_live_ key is resolvable.
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) no 401 polling loops.
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({