securenow 7.7.14 → 7.7.16

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/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({
@@ -596,11 +580,11 @@ if (loggingEnabled) {
596
580
  // request's error path isn't fully covered by the OTel library. We install
597
581
  // targeted process-level handlers to catch them and log at debug level instead
598
582
  // of crashing the host app.
599
- const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
583
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
600
584
  function _isOtlpTransientError(err) {
601
585
  if (!err) return false;
602
586
  if (_TRANSIENT_CODES.has(err.code)) return true;
603
- if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
587
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
604
588
  return false;
605
589
  }
606
590
  function _looksLikeOtlpStack(err) {
@@ -609,25 +593,58 @@ function _looksLikeOtlpStack(err) {
609
593
  return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
610
594
  || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
611
595
  }
596
+ function _looksLikeConfiguredOtlpEndpoint(err) {
597
+ const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
598
+ try {
599
+ const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
600
+ return hosts.some((host) => host && text.includes(host));
601
+ } catch (_) {
602
+ return false;
603
+ }
604
+ }
605
+ function _originalConsole(method) {
606
+ const originals = console.__securenow_original || console.__securenowOriginalConsole;
607
+ return (originals && originals[method]) || console[method] || console.log;
608
+ }
609
+ let _lastSuppressedOtlpErrorLogAt = 0;
610
+ function _formatOtlpError(err) {
611
+ if (!err) return 'unknown error';
612
+ const parts = [err.message || String(err)];
613
+ if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
614
+ if (err.syscall) parts.push(`syscall=${err.syscall}`);
615
+ if (err.hostname) parts.push(`host=${err.hostname}`);
616
+ return parts.join(' ');
617
+ }
618
+ function _reportSuppressedOtlpError(kind, err, origin) {
619
+ const level = String(diagLevel || '').toLowerCase();
620
+ if (level === 'none') return;
621
+ const now = Date.now();
622
+ if (level !== 'debug' && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
623
+ _lastSuppressedOtlpErrorLogAt = now;
624
+ const method = level === 'debug' ? 'debug' : 'error';
625
+ _originalConsole(method).call(
626
+ console,
627
+ '[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
628
+ kind,
629
+ origin || 'async',
630
+ _formatOtlpError(err)
631
+ );
632
+ }
612
633
 
613
634
  process.on('uncaughtException', (err, origin) => {
614
- if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
615
- if (diagLevel === 'debug') {
616
- console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
617
- }
618
- return; // swallow — do not crash
635
+ if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
636
+ _reportSuppressedOtlpError('error', err, origin);
637
+ return; // swallow - do not crash
619
638
  }
620
- // Not ours re-throw so the default handler (or the app's own handler) fires
639
+ // Not ours — re-throw so the default handler (or the app's own handler) fires
621
640
  throw err;
622
641
  });
623
642
  process.on('unhandledRejection', (reason) => {
624
- if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
625
- if (diagLevel === 'debug') {
626
- console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
627
- }
643
+ if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
644
+ _reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
628
645
  return; // swallow
629
646
  }
630
- // Not ours re-throw as unhandled so Node's default behaviour applies
647
+ // Not ours — re-throw as unhandled so Node's default behaviour applies
631
648
  throw reason;
632
649
  });
633
650
 
@@ -651,19 +668,19 @@ const sdk = new NodeSDK({
651
668
  (async () => {
652
669
  try {
653
670
  await Promise.resolve(sdk.start?.());
654
- console.log('[securenow] OTel SDK started %s', tracesUrl);
671
+ console.log('[securenow] OTel SDK started → %s', tracesUrl);
655
672
  if (loggingEnabled) {
656
- console.log('[securenow] 📋 Logging: ENABLED %s', logsUrl);
673
+ console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
657
674
  } else {
658
675
  console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
659
676
  }
660
677
  if (captureBody) {
661
- console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
678
+ console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
662
679
  }
663
680
  if (captureMultipart) {
664
- console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming file content not buffered)');
681
+ console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
665
682
  }
666
- if (String(env('SECURENOW_TEST_SPAN')) === '1') {
683
+ if (appConfig.boolConfig('runtime.testSpan', false)) {
667
684
  const api = require('@opentelemetry/api');
668
685
  const tracer = api.trace.getTracer('securenow-smoke');
669
686
  const span = tracer.startSpan('securenow.startup.smoke'); span.end();
@@ -671,13 +688,13 @@ const sdk = new NodeSDK({
671
688
 
672
689
  // Free trial banner
673
690
  const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
674
- if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
691
+ if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
675
692
  patchHttpForBanner();
676
693
  }
677
694
 
678
- // Firewall auto-activates only when a real snk_live_ key is resolvable.
695
+ // Firewall — auto-activates only when a real snk_live_ key is resolvable.
679
696
  // 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.
697
+ // only an app-routing UUID (or nothing at all) — no 401 polling loops.
681
698
  const firewallOptions = appConfig.resolveFirewallOptions();
682
699
  if (firewallOptions.apiKey && firewallOptions.enabled) {
683
700
  require('./firewall').init({
@@ -685,6 +702,7 @@ const sdk = new NodeSDK({
685
702
  appKey: firewallOptions.appKey,
686
703
  environment: firewallOptions.environment,
687
704
  apiUrl: firewallOptions.apiUrl,
705
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
688
706
  versionCheckInterval: firewallOptions.versionCheckInterval,
689
707
  syncInterval: firewallOptions.syncInterval,
690
708
  failMode: firewallOptions.failMode,
package/web-vite.mjs CHANGED
@@ -11,9 +11,24 @@ import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-u
11
11
  import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
12
12
  import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
13
13
 
14
- // ---- helpers / env ----
14
+ // ---- helpers / browser config ----
15
15
  const viteEnv = import.meta.env || {};
16
16
 
17
+ const ENV_TO_BROWSER_CONFIG_PATH = Object.freeze({
18
+ SECURENOW_APPID: 'app.key',
19
+ SECURENOW_INSTANCE: 'config.otel.endpoint',
20
+ OTEL_SERVICE_NAME: 'app.name',
21
+ OTEL_EXPORTER_OTLP_ENDPOINT: 'config.otel.endpoint',
22
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'config.otel.tracesEndpoint',
23
+ OTEL_EXPORTER_OTLP_HEADERS: 'config.otel.headers',
24
+ SECURENOW_NO_UUID: 'config.runtime.noUuid',
25
+ SECURENOW_STRICT: 'config.runtime.strict',
26
+ SECURENOW_TEST_SPAN: 'config.runtime.testSpan',
27
+ SECURENOW_HIDE_BANNER: 'config.runtime.hideBanner',
28
+ SECURENOW_ENVIRONMENT: 'config.runtime.deploymentEnvironment',
29
+ SECURENOW_DEPLOYMENT_ENVIRONMENT: 'config.runtime.deploymentEnvironment',
30
+ });
31
+
17
32
  function createResource(attributes) {
18
33
  if (typeof otelResources.resourceFromAttributes === 'function') {
19
34
  return otelResources.resourceFromAttributes(attributes);
@@ -24,17 +39,62 @@ function createResource(attributes) {
24
39
  throw new Error('Unsupported @opentelemetry/resources version');
25
40
  }
26
41
 
42
+ function legacyViteEnvFallbackEnabled() {
43
+ const raw =
44
+ viteEnv.SECURENOW_ENABLE_LEGACY_ENV ??
45
+ viteEnv.VITE_SECURENOW_ENABLE_LEGACY_ENV ??
46
+ viteEnv.SECURENOW_ALLOW_ENV_CONFIG ??
47
+ viteEnv.VITE_SECURENOW_ALLOW_ENV_CONFIG;
48
+ return /^(1|true|yes)$/i.test(String(raw || '').trim());
49
+ }
50
+
51
+ function getPath(obj, path) {
52
+ if (!obj || !path) return undefined;
53
+ let cur = obj;
54
+ for (const part of String(path).split('.')) {
55
+ if (cur == null || typeof cur !== 'object' || !(part in cur)) return undefined;
56
+ cur = cur[part];
57
+ }
58
+ return cur;
59
+ }
60
+
61
+ function headersToString(headers) {
62
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return undefined;
63
+ return Object.entries(headers)
64
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
65
+ .map(([key, value]) => `${key}=${value}`)
66
+ .join(',');
67
+ }
68
+
69
+ function toConfigString(value) {
70
+ if (value === undefined || value === null || value === '') return undefined;
71
+ if (typeof value === 'boolean') return value ? '1' : '0';
72
+ if (Array.isArray(value)) return value.join(',');
73
+ if (typeof value === 'object') return headersToString(value);
74
+ return String(value);
75
+ }
76
+
27
77
  function env(k) {
28
- // Accept both Vite envs (VITE_*) and raw names for window.__SECURENOW__
78
+ // Browser config should be injected as window.__SECURENOW__ by the app.
79
+ const w = globalThis.window;
80
+ const injected = w && w.__SECURENOW__;
81
+ const mappedPath = ENV_TO_BROWSER_CONFIG_PATH[String(k).toUpperCase()];
82
+ if (injected) {
83
+ if (mappedPath) {
84
+ const mappedValue = getPath(injected, mappedPath);
85
+ const resolved = toConfigString(mappedValue);
86
+ if (resolved !== undefined) return resolved;
87
+ }
88
+ if (!mappedPath && k in injected) return toConfigString(injected[k]);
89
+ }
90
+
91
+ // Vite env fallbacks are legacy and disabled by default.
92
+ if (!legacyViteEnvFallbackEnabled()) return undefined;
29
93
  const direct =
30
94
  viteEnv[k] ??
31
95
  viteEnv[k.toUpperCase()] ??
32
96
  viteEnv[k.toLowerCase()];
33
97
  if (direct != null) return String(direct);
34
-
35
- // Optionally support runtime overrides via window.__SECURENOW__
36
- const w = globalThis.window;
37
- if (w && w.__SECURENOW__ && k in w.__SECURENOW__) return String(w.__SECURENOW__[k]);
38
98
  return undefined;
39
99
  }
40
100
 
@@ -51,20 +111,50 @@ function parseHeaders(str) {
51
111
  return out;
52
112
  }
53
113
 
114
+ function normalizeEndpointBase(value) {
115
+ const endpoint = String(value || '').trim().replace(/\/$/, '');
116
+ if (!endpoint) return endpoint;
117
+ if (endpoint === 'https://api.securenow.ai/api/otlp') return 'https://ingest.securenow.ai';
118
+
119
+ try {
120
+ const parseable = /^[a-z][a-z0-9+.-]*:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`;
121
+ const url = new URL(parseable);
122
+ if (url.port === '4318' && url.hostname.toLowerCase().endsWith('.securenow.ai')) {
123
+ return 'https://ingest.securenow.ai';
124
+ }
125
+ } catch {}
126
+
127
+ return endpoint;
128
+ }
129
+
130
+ function normalizeSignalEndpoint(value, signalType) {
131
+ if (!value) return value;
132
+ const endpoint = String(value).trim().replace(/\/$/, '');
133
+ const signalPath = `/v1/${signalType}`;
134
+ if (endpoint.endsWith(signalPath)) {
135
+ return `${normalizeEndpointBase(endpoint.slice(0, -signalPath.length))}${signalPath}`;
136
+ }
137
+ return endpoint;
138
+ }
139
+
54
140
  // ---- endpoints (same defaults as tracing.js) ----
55
141
  const endpointBase =
56
- (env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'https://freetrial.securenow.ai:4318')
57
- .replace(/\/$/, '');
142
+ normalizeEndpointBase(env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'https://ingest.securenow.ai');
58
143
  const tracesUrl =
59
- env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
144
+ normalizeSignalEndpoint(env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'), 'traces') || `${endpointBase}/v1/traces`;
60
145
  const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
146
+ const deploymentEnvironment =
147
+ env('SECURENOW_DEPLOYMENT_ENVIRONMENT') || env('SECURENOW_ENVIRONMENT') || viteEnv.MODE || 'production';
148
+ const browserAppKey = env('SECURENOW_APPID');
149
+ if (browserAppKey && !headers['x-api-key']) headers['x-api-key'] = browserAppKey;
150
+ if (deploymentEnvironment && !headers['x-securenow-environment']) headers['x-securenow-environment'] = deploymentEnvironment;
61
151
 
62
152
  // ---- naming rules (mirrors tracing.js) ----
63
153
  const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
64
154
  const baseName = rawBase || null;
65
155
  // Default to no suffix whenever a baseName is resolved: the dashboard filters
66
156
  // service.name by exact match, and browsers have no PM2 cluster problem
67
- // (each tab has its own service.instance.id). Explicit SECURENOW_NO_UUID=0
157
+ // (each tab has its own service.instance.id). Explicit config.runtime.noUuid=false
68
158
  // still re-enables the suffix if someone really wants it.
69
159
  const noUuidEnv = env('SECURENOW_NO_UUID');
70
160
  const noUuid =
@@ -95,7 +185,7 @@ if (baseName) {
95
185
  serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
96
186
  } else {
97
187
  if (strict) {
98
- console.error('[securenow/web-vite] FATAL: SECURENOW_APPID/OTEL_SERVICE_NAME missing and SECURENOW_STRICT=1. Tracing disabled.');
188
+ console.error('[securenow/web-vite] FATAL: SecureNow app key missing and config.runtime.strict=true. Tracing disabled.');
99
189
  // @ts-expect-error
100
190
  window.__SECURENOW_DISABLED__ = true;
101
191
  disabled = true;
@@ -112,7 +202,7 @@ const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
112
202
  try {
113
203
  // eslint-disable-next-line no-console
114
204
  console.log(
115
- '[securenow] web preload loaded SECURENOW_APPID=%s OTEL_SERVICE_NAME=%s SECURENOW_NO_UUID=%s SECURENOW_STRICT=%s → service.name=%s instance.id=%s',
205
+ '[securenow] web preload loaded app.key=%s app.name=%s config.runtime.noUuid=%s config.runtime.strict=%s → service.name=%s instance.id=%s',
116
206
  JSON.stringify(env('SECURENOW_APPID')),
117
207
  JSON.stringify(env('OTEL_SERVICE_NAME')),
118
208
  JSON.stringify(env('SECURENOW_NO_UUID')),
@@ -138,7 +228,7 @@ export function startSecurenowWeb() {
138
228
  resource: createResource({
139
229
  [S.SERVICE_NAME]: serviceName,
140
230
  [S.SERVICE_INSTANCE_ID]: serviceInstanceId,
141
- [S.DEPLOYMENT_ENVIRONMENT]: viteEnv.MODE || 'production',
231
+ [S.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
142
232
  [S.SERVICE_VERSION]: viteEnv.VITE_APP_VERSION || undefined,
143
233
  }),
144
234
  spanProcessors: [new BatchSpanProcessor(exporter)],
@@ -174,9 +264,9 @@ export function startSecurenowWeb() {
174
264
 
175
265
  // ---- Free trial banner (browser DOM injection) ----
176
266
  function injectFreeTrialBanner() {
177
- const FREETRIAL_HOST = 'freetrial.securenow.ai';
267
+ const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
178
268
  const hideBanner = String(env('SECURENOW_HIDE_BANNER')) === '1';
179
- if (hideBanner || !endpointBase.includes(FREETRIAL_HOST)) return;
269
+ if (hideBanner || !FREE_TRIAL_HOSTS.some(host => endpointBase.includes(host))) return;
180
270
  if (typeof document === 'undefined') return;
181
271
 
182
272
  function create() {