securenow 7.7.15 → 7.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/server.js CHANGED
@@ -43,29 +43,83 @@ function fail(id, code, message, data) {
43
43
  });
44
44
  }
45
45
 
46
- function bearerToken() {
47
- return config.getToken() || config.getApiKey();
46
+ const RUNTIME_READ_SCOPES = new Set(['firewall:read', 'blocklist:read', 'allowlist:read']);
47
+
48
+ function toolCanUseRuntimeApiKey(tool) {
49
+ return tool && tool.readOnly !== false && RUNTIME_READ_SCOPES.has(tool.scope);
50
+ }
51
+
52
+ function withRuntimeAppDefaults(tool, args = {}) {
53
+ const runtimeApp = config.getApp();
54
+ if (!runtimeApp?.key) return args;
55
+ const next = { ...args };
56
+ const fields = new Set([
57
+ ...(tool.queryFields || []),
58
+ ...(tool.bodyFields || []),
59
+ ...(tool.pathParams || []),
60
+ ]);
61
+
62
+ if (fields.has('appKey') && !next.appKey) next.appKey = runtimeApp.key;
63
+ if (fields.has('applicationKey') && !next.applicationKey) next.applicationKey = runtimeApp.key;
64
+ if (fields.has('appKeys') && !next.appKeys) next.appKeys = runtimeApp.key;
65
+ return next;
66
+ }
67
+
68
+ function requiresExplicitOrRuntimeApp(tool) {
69
+ const required = new Set(tool?.inputSchema?.required || []);
70
+ return required.has('appKey') || required.has('appKeys') || required.has('applicationKey');
71
+ }
72
+
73
+ function hasAppScopeArg(args = {}) {
74
+ return !!(args.appKey || args.appKeys || args.applicationKey);
75
+ }
76
+
77
+ function bearerForTool(tool) {
78
+ const adminToken = config.getToken();
79
+ if (adminToken) return { token: adminToken, plane: 'admin' };
80
+
81
+ const runtimeKey = config.getApiKey();
82
+ if (runtimeKey && toolCanUseRuntimeApiKey(tool)) {
83
+ return { token: runtimeKey, plane: 'runtime' };
84
+ }
85
+
86
+ return { token: null, plane: toolCanUseRuntimeApiKey(tool) ? 'runtime' : 'admin' };
48
87
  }
49
88
 
50
89
  function localAuthStatus() {
51
90
  const cfg = config.loadConfig();
52
91
  const app = config.getApp();
92
+ const admin = config.loadAdminCredentials();
93
+ const runtime = config.loadRuntimeCredentials();
53
94
  const token = config.getToken();
54
95
  const apiKey = config.getApiKey();
55
96
  const hasBearer = !!(token || apiKey);
56
- const runtimeFirewallWarning = token && !apiKey
57
- ? 'MCP is authenticated with your SecureNow session token. Runtime firewall enforcement key is missing. Run `npx securenow login` or `npx securenow api-key set snk_live_...` to refresh .securenow/credentials.json.'
97
+ const runtimeFirewallWarning = app && !apiKey
98
+ ? 'Runtime app is connected, but the firewall key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` to refresh .securenow/runtime.json.'
58
99
  : null;
59
100
  return {
60
101
  authenticated: hasBearer,
102
+ adminAuthenticated: !!token,
103
+ runtimeConnected: !!app?.key,
61
104
  sessionTokenAvailable: !!token,
62
105
  apiKeyAvailable: !!apiKey,
63
106
  runtimeFirewallKeyAvailable: !!apiKey,
64
- authSource: config.getAuthSource(),
107
+ adminAuthSource: token ? config.getAuthSource() : null,
108
+ runtimeSource: config.getRuntimeSource(),
65
109
  apiUrl: config.getApiUrl(),
66
110
  appUrl: config.getAppUrl(),
67
111
  defaultApp: config.getDefaultApp(),
68
112
  app: app || null,
113
+ admin: token ? {
114
+ email: admin.email || null,
115
+ expiresAt: admin.expiresAt || null,
116
+ systemRuleAdmin: 'server-enforced',
117
+ } : null,
118
+ runtime: {
119
+ app: runtime.app || null,
120
+ environment: runtime.config?.runtime?.deploymentEnvironment || null,
121
+ apiKeyAvailable: !!apiKey,
122
+ },
69
123
  token: token ? maskSecret(token) : null,
70
124
  apiKey: apiKey ? maskSecret(apiKey) : null,
71
125
  config: {
@@ -76,10 +130,10 @@ function localAuthStatus() {
76
130
  },
77
131
  warnings: runtimeFirewallWarning ? [runtimeFirewallWarning] : [],
78
132
  nextStep: token
79
- ? runtimeFirewallWarning
133
+ ? (runtimeFirewallWarning || (!app?.key ? 'Run `npx securenow app connect` to connect SDK runtime to an app.' : null))
80
134
  : apiKey
81
- ? 'Using a scoped API key. Run `npx securenow login` for account-wide MCP tools.'
82
- : 'Run `npx securenow login` from the project root.',
135
+ ? 'Using runtime API key for limited runtime read tools. Run `npx securenow admin login` for account-wide MCP tools.'
136
+ : 'Run `npx securenow admin login` for control-plane tools and `npx securenow app connect` for SDK runtime.',
83
137
  };
84
138
  }
85
139
 
@@ -94,12 +148,19 @@ async function callApiTool(tool, args) {
94
148
  throw new Error(`${tool.name} is only available in the local MCP server.`);
95
149
  }
96
150
 
97
- const token = bearerToken();
151
+ const finalArgs = withRuntimeAppDefaults(tool, args);
152
+ if (requiresExplicitOrRuntimeApp(tool) && !hasAppScopeArg(finalArgs)) {
153
+ throw new Error(`${tool.name} requires an app scope. Pass applicationKey/appKey/appKeys explicitly or run \`npx securenow app connect\` to configure runtime app credentials.`);
154
+ }
155
+ const { token, plane } = bearerForTool(tool);
98
156
  if (!token) {
99
- throw new Error('Not authenticated. Run `npx securenow login` from the project root, then retry.');
157
+ if (plane === 'runtime') {
158
+ throw new Error('Runtime credentials are missing. Run `npx securenow app connect` or pass an explicit app key, then retry.');
159
+ }
160
+ throw new Error('Admin auth is missing. Run `npx securenow admin login` from the project root, then retry. Runtime app credentials cannot grant admin/control-plane permissions.');
100
161
  }
101
162
 
102
- const request = buildApiRequest(tool, args);
163
+ const request = buildApiRequest(tool, finalArgs);
103
164
  const options = {
104
165
  token,
105
166
  ...(Object.keys(request.query || {}).length > 0 ? { query: request.query } : {}),
@@ -197,7 +258,7 @@ async function handleRequest(message) {
197
258
  const result = await callApiTool(tool, args);
198
259
  return ok(id, asToolResult({
199
260
  tool: name,
200
- arguments: sanitizeArgs(args),
261
+ arguments: sanitizeArgs(withRuntimeAppDefaults(tool, args)),
201
262
  result,
202
263
  }));
203
264
  }
package/nextjs.js CHANGED
@@ -31,10 +31,13 @@
31
31
  */
32
32
 
33
33
  const { randomUUID } = require('crypto');
34
+ const { defaultMetricsExporterToNone } = require('./otel-defaults');
34
35
  const appConfig = require('./app-config');
35
36
  const { resolveClientIpWithDetails } = require('./resolve-ip');
36
37
  const otelResources = require('@opentelemetry/resources');
37
38
 
39
+ defaultMetricsExporterToNone();
40
+
38
41
  let isRegistered = false;
39
42
 
40
43
  function requireRuntimeModule(name) {
@@ -189,6 +192,8 @@ function registerSecureNow(options = {}) {
189
192
  if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
190
193
  if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
191
194
  if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
195
+ const headerString = appConfig.headersToString(headers);
196
+ if (headerString && !process.env.OTEL_EXPORTER_OTLP_HEADERS) process.env.OTEL_EXPORTER_OTLP_HEADERS = headerString;
192
197
  }
193
198
 
194
199
  console.log('[securenow] Next.js App -> service.name=%s', serviceName);
@@ -376,11 +381,11 @@ function registerSecureNow(options = {}) {
376
381
  // the remote end (ECONNRESET / "socket hang up"). These transient errors
377
382
  // sometimes escape as unhandled exceptions or rejections. We catch them
378
383
  // here and log at debug level instead of crashing the host app.
379
- const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
384
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
380
385
  function _isOtlpTransientError(err) {
381
386
  if (!err) return false;
382
387
  if (_TRANSIENT_CODES.has(err.code)) return true;
383
- if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
388
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
384
389
  return false;
385
390
  }
386
391
  function _looksLikeOtlpStack(err) {
@@ -389,21 +394,53 @@ function registerSecureNow(options = {}) {
389
394
  return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
390
395
  || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
391
396
  }
397
+ function _looksLikeConfiguredOtlpEndpoint(err) {
398
+ const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
399
+ try {
400
+ const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
401
+ return hosts.some((host) => host && text.includes(host));
402
+ } catch (_) {
403
+ return false;
404
+ }
405
+ }
392
406
  const _diagDebug = otelLogLevel === 'debug';
407
+ let _lastSuppressedOtlpErrorLogAt = 0;
408
+ function _originalConsole(method) {
409
+ const originals = console.__securenow_original || console.__securenowOriginalConsole;
410
+ return (originals && originals[method]) || console[method] || console.log;
411
+ }
412
+ function _formatOtlpError(err) {
413
+ if (!err) return 'unknown error';
414
+ const parts = [err.message || String(err)];
415
+ if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
416
+ if (err.syscall) parts.push(`syscall=${err.syscall}`);
417
+ if (err.hostname) parts.push(`host=${err.hostname}`);
418
+ return parts.join(' ');
419
+ }
420
+ function _reportSuppressedOtlpError(kind, err, origin) {
421
+ if (otelLogLevel === 'none') return;
422
+ const now = Date.now();
423
+ if (!_diagDebug && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
424
+ _lastSuppressedOtlpErrorLogAt = now;
425
+ const method = _diagDebug ? 'debug' : 'error';
426
+ _originalConsole(method).call(
427
+ console,
428
+ '[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
429
+ kind,
430
+ origin || 'async',
431
+ _formatOtlpError(err)
432
+ );
433
+ }
393
434
  process.on('uncaughtException', (err, origin) => {
394
- if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
395
- if (_diagDebug) {
396
- console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
397
- }
435
+ if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
436
+ _reportSuppressedOtlpError('error', err, origin);
398
437
  return;
399
438
  }
400
439
  throw err;
401
440
  });
402
441
  process.on('unhandledRejection', (reason) => {
403
- if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
404
- if (_diagDebug) {
405
- console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
406
- }
442
+ if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
443
+ _reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
407
444
  return;
408
445
  }
409
446
  throw reason;
@@ -625,13 +662,14 @@ function registerSecureNow(options = {}) {
625
662
  // Key and environment come from .securenow/credentials.json (written by
626
663
  // login/init or credentials runtime), so no .env entry is needed.
627
664
  const firewallOptions = appConfig.resolveFirewallOptions();
628
- if (firewallOptions.apiKey && firewallOptions.enabled) {
665
+ if (firewallOptions.apiKey) {
629
666
  try {
630
667
  requireRuntimeModule('./firewall').init({
631
668
  apiKey: firewallOptions.apiKey,
632
669
  appKey: firewallOptions.appKey,
633
670
  environment: deploymentEnvironment || firewallOptions.environment,
634
671
  apiUrl: firewallOptions.apiUrl,
672
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
635
673
  versionCheckInterval: firewallOptions.versionCheckInterval,
636
674
  syncInterval: firewallOptions.syncInterval,
637
675
  failMode: firewallOptions.failMode,
@@ -7,11 +7,8 @@
7
7
  * This file is registered by the Nuxt module (nuxt.mjs) via addServerPlugin.
8
8
  */
9
9
 
10
- import { NodeSDK } from '@opentelemetry/sdk-node';
11
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
12
10
  import * as otelResources from '@opentelemetry/resources';
13
11
  import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
14
- import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
15
12
  import {
16
13
  context as otelContext,
17
14
  trace as otelTrace,
@@ -23,6 +20,12 @@ import { randomUUID } from 'node:crypto';
23
20
  const nodeRequire = createRequire(import.meta.url);
24
21
  const appConfig = nodeRequire('./app-config');
25
22
  const { resolveClientIpWithDetails } = nodeRequire('./resolve-ip');
23
+ const { defaultMetricsExporterToNone } = nodeRequire('./otel-defaults');
24
+ defaultMetricsExporterToNone();
25
+
26
+ const { NodeSDK } = nodeRequire('@opentelemetry/sdk-node');
27
+ const { OTLPTraceExporter } = nodeRequire('@opentelemetry/exporter-trace-otlp-http');
28
+ const { HttpInstrumentation } = nodeRequire('@opentelemetry/instrumentation-http');
26
29
 
27
30
  // ── Helpers ──
28
31
 
@@ -318,7 +321,7 @@ export default defineNitroPlugin(async (nitroApp) => {
318
321
 
319
322
  // ── Firewall — runs independently from OTel ──
320
323
  const firewallOptions = appConfig.resolveFirewallOptions();
321
- if (firewallOptions.apiKey && firewallOptions.enabled) {
324
+ if (firewallOptions.apiKey) {
322
325
  try {
323
326
  const { init: fwInit } = await import('./firewall.js');
324
327
  fwInit({
@@ -326,6 +329,7 @@ export default defineNitroPlugin(async (nitroApp) => {
326
329
  appKey: firewallOptions.appKey,
327
330
  environment: deploymentEnvironment || firewallOptions.environment,
328
331
  apiUrl: firewallOptions.apiUrl,
332
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
329
333
  versionCheckInterval: firewallOptions.versionCheckInterval,
330
334
  syncInterval: firewallOptions.syncInterval,
331
335
  failMode: firewallOptions.failMode,
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ function defaultMetricsExporterToNone(env = process.env) {
4
+ if (!env) return false;
5
+ const current = env.OTEL_METRICS_EXPORTER;
6
+ if (current != null && String(current).trim() !== '') return false;
7
+ env.OTEL_METRICS_EXPORTER = 'none';
8
+ return true;
9
+ }
10
+
11
+ module.exports = { defaultMetricsExporterToNone };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.15",
3
+ "version": "7.8.1",
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",
@@ -132,6 +132,7 @@
132
132
  "resolve-ip.js",
133
133
  "cidr.js",
134
134
  "firewall.js",
135
+ "otel-defaults.js",
135
136
  "rate-limits.js",
136
137
  "rate-limits.d.ts",
137
138
  "firewall-only.js",
package/tracing.js CHANGED
@@ -14,6 +14,9 @@
14
14
  * Production should mount/copy tokenless runtime credentials to the same path.
15
15
  */
16
16
 
17
+ const { defaultMetricsExporterToNone } = require('./otel-defaults');
18
+ defaultMetricsExporterToNone();
19
+
17
20
  const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
18
21
  const { NodeSDK } = require('@opentelemetry/sdk-node');
19
22
  const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
@@ -580,11 +583,11 @@ if (loggingEnabled) {
580
583
  // request's error path isn't fully covered by the OTel library. We install
581
584
  // targeted process-level handlers to catch them and log at debug level instead
582
585
  // of crashing the host app.
583
- const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
586
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
584
587
  function _isOtlpTransientError(err) {
585
588
  if (!err) return false;
586
589
  if (_TRANSIENT_CODES.has(err.code)) return true;
587
- if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
590
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
588
591
  return false;
589
592
  }
590
593
  function _looksLikeOtlpStack(err) {
@@ -593,22 +596,55 @@ function _looksLikeOtlpStack(err) {
593
596
  return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
594
597
  || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
595
598
  }
599
+ function _looksLikeConfiguredOtlpEndpoint(err) {
600
+ const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
601
+ try {
602
+ const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
603
+ return hosts.some((host) => host && text.includes(host));
604
+ } catch (_) {
605
+ return false;
606
+ }
607
+ }
608
+ function _originalConsole(method) {
609
+ const originals = console.__securenow_original || console.__securenowOriginalConsole;
610
+ return (originals && originals[method]) || console[method] || console.log;
611
+ }
612
+ let _lastSuppressedOtlpErrorLogAt = 0;
613
+ function _formatOtlpError(err) {
614
+ if (!err) return 'unknown error';
615
+ const parts = [err.message || String(err)];
616
+ if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
617
+ if (err.syscall) parts.push(`syscall=${err.syscall}`);
618
+ if (err.hostname) parts.push(`host=${err.hostname}`);
619
+ return parts.join(' ');
620
+ }
621
+ function _reportSuppressedOtlpError(kind, err, origin) {
622
+ const level = String(diagLevel || '').toLowerCase();
623
+ if (level === 'none') return;
624
+ const now = Date.now();
625
+ if (level !== 'debug' && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
626
+ _lastSuppressedOtlpErrorLogAt = now;
627
+ const method = level === 'debug' ? 'debug' : 'error';
628
+ _originalConsole(method).call(
629
+ console,
630
+ '[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
631
+ kind,
632
+ origin || 'async',
633
+ _formatOtlpError(err)
634
+ );
635
+ }
596
636
 
597
637
  process.on('uncaughtException', (err, origin) => {
598
- if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
599
- if (diagLevel === 'debug') {
600
- console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
601
- }
602
- return; // swallow — do not crash
638
+ if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
639
+ _reportSuppressedOtlpError('error', err, origin);
640
+ return; // swallow - do not crash
603
641
  }
604
642
  // Not ours — re-throw so the default handler (or the app's own handler) fires
605
643
  throw err;
606
644
  });
607
645
  process.on('unhandledRejection', (reason) => {
608
- if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
609
- if (diagLevel === 'debug') {
610
- console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
611
- }
646
+ if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
647
+ _reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
612
648
  return; // swallow
613
649
  }
614
650
  // Not ours — re-throw as unhandled so Node's default behaviour applies
@@ -663,12 +699,13 @@ const sdk = new NodeSDK({
663
699
  // resolveApiKey() enforces the prefix, so we skip cleanly when the app has
664
700
  // only an app-routing UUID (or nothing at all) — no 401 polling loops.
665
701
  const firewallOptions = appConfig.resolveFirewallOptions();
666
- if (firewallOptions.apiKey && firewallOptions.enabled) {
702
+ if (firewallOptions.apiKey) {
667
703
  require('./firewall').init({
668
704
  apiKey: firewallOptions.apiKey,
669
705
  appKey: firewallOptions.appKey,
670
706
  environment: firewallOptions.environment,
671
707
  apiUrl: firewallOptions.apiUrl,
708
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
672
709
  versionCheckInterval: firewallOptions.versionCheckInterval,
673
710
  syncInterval: firewallOptions.syncInterval,
674
711
  failMode: firewallOptions.failMode,
package/web-vite.mjs CHANGED
@@ -145,6 +145,9 @@ const tracesUrl =
145
145
  const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
146
146
  const deploymentEnvironment =
147
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;
148
151
 
149
152
  // ---- naming rules (mirrors tracing.js) ----
150
153
  const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');