securenow 7.7.16 → 8.0.0

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 runtime API 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
  }
@@ -18,7 +18,6 @@
18
18
 
19
19
  const { trace } = require('@opentelemetry/api');
20
20
  const appConfig = require('./app-config');
21
- const env = appConfig.env;
22
21
 
23
22
  // Default sensitive fields to redact
24
23
  const DEFAULT_SENSITIVE_FIELDS = [
@@ -56,8 +55,8 @@ async function safeBodyCapture(request, span) {
56
55
 
57
56
  try {
58
57
  const contentType = request.headers.get('content-type') || '';
59
- const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
60
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
58
+ const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
59
+ const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
61
60
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
62
61
 
63
62
  // Only for supported types
@@ -127,8 +126,7 @@ async function safeBodyCapture(request, span) {
127
126
  * Check if body capture is enabled
128
127
  */
129
128
  function isBodyCaptureEnabled() {
130
- // Opt-out default: set config.capture.body=false to disable.
131
- return !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
129
+ return appConfig.boolConfig('capture.body', true);
132
130
  }
133
131
 
134
132
  /**
@@ -195,4 +193,3 @@ module.exports = {
195
193
  isBodyCaptureEnabled,
196
194
  };
197
195
 
198
-
@@ -17,7 +17,6 @@
17
17
 
18
18
  const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
19
19
  const appConfig = require('./app-config');
20
- const env = appConfig.env;
21
20
 
22
21
  // Default sensitive fields to redact
23
22
  const DEFAULT_SENSITIVE_FIELDS = [
@@ -101,8 +100,8 @@ async function middleware(request) {
101
100
 
102
101
  try {
103
102
  const contentType = request.headers.get('content-type') || '';
104
- const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
105
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
103
+ const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
104
+ const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
106
105
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
107
106
 
108
107
  // Only capture supported types
@@ -184,4 +183,3 @@ module.exports = {
184
183
  };
185
184
 
186
185
 
187
-
package/nextjs-wrapper.js CHANGED
@@ -17,7 +17,6 @@
17
17
 
18
18
  const { trace } = require('@opentelemetry/api');
19
19
  const appConfig = require('./app-config');
20
- const env = appConfig.env;
21
20
 
22
21
  // Default sensitive fields to redact
23
22
  const DEFAULT_SENSITIVE_FIELDS = [
@@ -51,8 +50,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
51
50
  * Capture body from Request object (clone to avoid consuming)
52
51
  */
53
52
  async function captureRequestBody(request) {
54
- // Opt-out default: set config.capture.body=false to disable.
55
- const captureBody = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
53
+ const captureBody = appConfig.boolConfig('capture.body', true);
56
54
 
57
55
  if (!captureBody) return;
58
56
  if (!['POST', 'PUT', 'PATCH'].includes(request.method)) return;
@@ -62,8 +60,8 @@ async function captureRequestBody(request) {
62
60
 
63
61
  try {
64
62
  const contentType = request.headers.get('content-type') || '';
65
- const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
66
- const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
63
+ const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
64
+ const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
67
65
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
68
66
 
69
67
  // Only for supported types
@@ -155,4 +153,3 @@ module.exports = {
155
153
  DEFAULT_SENSITIVE_FIELDS,
156
154
  };
157
155
 
158
-
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) {
@@ -183,16 +186,6 @@ function registerSecureNow(options = {}) {
183
186
  const otelLogLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
184
187
  const isDevelopmentRuntime = process.env.NODE_ENV === 'development';
185
188
 
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
- const headerString = appConfig.headersToString(headers);
193
- if (headerString && !process.env.OTEL_EXPORTER_OTLP_HEADERS) process.env.OTEL_EXPORTER_OTLP_HEADERS = headerString;
194
- }
195
-
196
189
  console.log('[securenow] Next.js App -> service.name=%s', serviceName);
197
190
  console.log('[securenow] Environment: %s', deploymentEnvironment);
198
191
 
@@ -659,7 +652,7 @@ function registerSecureNow(options = {}) {
659
652
  // Key and environment come from .securenow/credentials.json (written by
660
653
  // login/init or credentials runtime), so no .env entry is needed.
661
654
  const firewallOptions = appConfig.resolveFirewallOptions();
662
- if (firewallOptions.apiKey && firewallOptions.enabled) {
655
+ if (firewallOptions.apiKey) {
663
656
  try {
664
657
  requireRuntimeModule('./firewall').init({
665
658
  apiKey: firewallOptions.apiKey,
@@ -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({
@@ -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.16",
3
+ "version": "8.0.0",
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",
@@ -166,8 +167,7 @@
166
167
  "@opentelemetry/sdk-node": "0.218.0",
167
168
  "@opentelemetry/sdk-trace-base": "2.7.1",
168
169
  "@opentelemetry/sdk-trace-web": "2.7.1",
169
- "@opentelemetry/semantic-conventions": "1.41.1",
170
- "dotenv": "17.2.1"
170
+ "@opentelemetry/semantic-conventions": "1.41.1"
171
171
  },
172
172
  "optionalDependencies": {
173
173
  "@vercel/otel": "2.1.2"
package/rate-limits.js CHANGED
@@ -12,8 +12,6 @@ function authToken(options = {}) {
12
12
  options.token ||
13
13
  options.apiKey ||
14
14
  options.authToken ||
15
- process.env.SECURENOW_TOKEN ||
16
- process.env.SECURENOW_API_KEY ||
17
15
  config.getToken() ||
18
16
  config.getApiKey() ||
19
17
  undefined
package/register-vite.js CHANGED
@@ -1,12 +1,5 @@
1
- // node_modules/securenow/register-vite.js (CommonJS, for -r)
2
- 'use strict';
3
-
4
- try {
5
- require('dotenv').config();
6
- console.log('[securenow] dotenv loaded for Vite preload');
7
- } catch {
8
- console.log('[securenow] dotenv not available, continuing without .env');
9
- }
10
-
11
- // Trace the Node process (Vite dev/preview server) using your existing Node tracer
12
- module.exports = require('./web-vite.mjs');
1
+ // node_modules/securenow/register-vite.js (CommonJS, for -r)
2
+ 'use strict';
3
+
4
+ // Trace the Node process (Vite dev/preview server) using your existing Node tracer
5
+ module.exports = require('./web-vite.mjs');
package/register.js CHANGED
@@ -4,17 +4,9 @@
4
4
  // For ESM apps ("type": "module"), this file auto-registers the
5
5
  // OpenTelemetry ESM loader hook via module.register() (Node >=20.6).
6
6
  // On older Node versions it falls back to a warning.
7
- 'use strict';
8
-
9
- // 1. Load dotenv quietly only for legacy installs. Normal local and production
10
- // configuration comes from .securenow/credentials.json via app-config.js.
11
- try {
12
- require('dotenv').config({ quiet: true });
13
- } catch (e) {
14
- // dotenv is optional.
15
- }
16
-
17
- // 2. Auto-register the ESM loader hook so customers never need --import
7
+ 'use strict';
8
+
9
+ // 1. Auto-register the ESM loader hook so customers never need --import
18
10
  (() => {
19
11
  try {
20
12
  const fs = require('fs');
@@ -45,5 +37,5 @@ try {
45
37
  }
46
38
  })();
47
39
 
48
- // 3. Run the OTel SDK setup
49
- require('./tracing');
40
+ // 2. Run the OTel SDK setup
41
+ require('./tracing');
package/resolve-ip.js CHANGED
@@ -6,7 +6,7 @@ const appConfig = require('./app-config');
6
6
  const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
7
7
  const PRIVATE_IP_RE = /^(127\.|::1$|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|f[cd][0-9a-f]{2}:)/i;
8
8
 
9
- const trustedProxies = appConfig.listEnv('SECURENOW_TRUSTED_PROXIES');
9
+ const trustedProxies = appConfig.listConfig('networking.trustedProxies');
10
10
  const trustedProxySet = trustedProxies.length ? new Set(trustedProxies.map(normalizeIp).filter(Boolean)) : null;
11
11
 
12
12
  let _hostIps = null;
package/tracing.d.ts CHANGED
@@ -165,7 +165,7 @@ export const loggerProvider: LoggerProvider | null;
165
165
  * Configuration:
166
166
  *
167
167
  * Local development reads .securenow/credentials.json first. Run
168
- * `npx securenow login` to write app identity/firewall key and
168
+ * `npx securenow login` to write app identity/runtime API key and
169
169
  * `npx securenow init` to ensure secure defaults and explanations.
170
170
  *
171
171
  * Production uses the same file shape. Run
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');
@@ -277,8 +280,8 @@ const endpointBase = resolvedEndpoints.endpointBase;
277
280
  const tracesUrl = resolvedEndpoints.tracesUrl;
278
281
  const logsUrl = resolvedEndpoints.logsUrl;
279
282
 
280
- // resolveEndpoints() also adds x-api-key from the credentials app key when
281
- // explicit OTLP headers did not provide one.
283
+ // resolveEndpoints() also adds app routing and runtime auth headers when
284
+ // explicit OTLP headers did not provide them.
282
285
  const headers = resolvedEndpoints.headers;
283
286
 
284
287
  // -------- naming rules --------
@@ -696,7 +699,7 @@ const sdk = new NodeSDK({
696
699
  // resolveApiKey() enforces the prefix, so we skip cleanly when the app has
697
700
  // only an app-routing UUID (or nothing at all) — no 401 polling loops.
698
701
  const firewallOptions = appConfig.resolveFirewallOptions();
699
- if (firewallOptions.apiKey && firewallOptions.enabled) {
702
+ if (firewallOptions.apiKey) {
700
703
  require('./firewall').init({
701
704
  apiKey: firewallOptions.apiKey,
702
705
  appKey: firewallOptions.appKey,
package/web-vite.mjs CHANGED
@@ -12,21 +12,21 @@ import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
12
12
  import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
13
13
 
14
14
  // ---- helpers / browser config ----
15
- const viteEnv = import.meta.env || {};
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',
15
+ const DEFAULT_BROWSER_CONFIG = Object.freeze({
16
+ config: {
17
+ otel: {
18
+ endpoint: 'https://ingest.securenow.ai',
19
+ tracesEndpoint: null,
20
+ headers: {},
21
+ },
22
+ runtime: {
23
+ deploymentEnvironment: 'production',
24
+ noUuid: null,
25
+ strict: false,
26
+ testSpan: false,
27
+ hideBanner: false,
28
+ },
29
+ },
30
30
  });
31
31
 
32
32
  function createResource(attributes) {
@@ -39,15 +39,6 @@ function createResource(attributes) {
39
39
  throw new Error('Unsupported @opentelemetry/resources version');
40
40
  }
41
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
42
  function getPath(obj, path) {
52
43
  if (!obj || !path) return undefined;
53
44
  let cur = obj;
@@ -74,28 +65,27 @@ function toConfigString(value) {
74
65
  return String(value);
75
66
  }
76
67
 
77
- function env(k) {
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
- }
68
+ function browserConfig() {
69
+ const injected = globalThis.window && globalThis.window.__SECURENOW__;
70
+ return injected && typeof injected === 'object' ? injected : {};
71
+ }
90
72
 
91
- // Vite env fallbacks are legacy and disabled by default.
92
- if (!legacyViteEnvFallbackEnabled()) return undefined;
93
- const direct =
94
- viteEnv[k] ??
95
- viteEnv[k.toUpperCase()] ??
96
- viteEnv[k.toLowerCase()];
97
- if (direct != null) return String(direct);
98
- return undefined;
73
+ function configValue(path, fallback) {
74
+ const injected = getPath(browserConfig(), path);
75
+ if (injected !== undefined && injected !== null && injected !== '') return injected;
76
+ const defaultValue = getPath(DEFAULT_BROWSER_CONFIG, path);
77
+ if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '') return defaultValue;
78
+ return fallback;
79
+ }
80
+
81
+ function boolConfig(path, fallback = false) {
82
+ const value = configValue(path, fallback);
83
+ if (typeof value === 'boolean') return value;
84
+ if (typeof value === 'number') return value !== 0;
85
+ const text = String(value || '').trim().toLowerCase();
86
+ if (['1', 'true', 'yes', 'on', 'enabled'].includes(text)) return true;
87
+ if (['0', 'false', 'no', 'off', 'disabled'].includes(text)) return false;
88
+ return fallback;
99
89
  }
100
90
 
101
91
  function parseHeaders(str) {
@@ -139,29 +129,30 @@ function normalizeSignalEndpoint(value, signalType) {
139
129
 
140
130
  // ---- endpoints (same defaults as tracing.js) ----
141
131
  const endpointBase =
142
- normalizeEndpointBase(env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'https://ingest.securenow.ai');
132
+ normalizeEndpointBase(configValue('config.otel.endpoint', 'https://ingest.securenow.ai'));
143
133
  const tracesUrl =
144
- normalizeSignalEndpoint(env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'), 'traces') || `${endpointBase}/v1/traces`;
145
- const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
134
+ normalizeSignalEndpoint(configValue('config.otel.tracesEndpoint'), 'traces') || `${endpointBase}/v1/traces`;
135
+ const headers = parseHeaders(toConfigString(configValue('config.otel.headers', {})));
146
136
  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;
137
+ String(configValue('config.runtime.deploymentEnvironment', 'production'));
138
+ const browserAppKey = toConfigString(configValue('app.key'));
139
+ if (browserAppKey && !headers['x-securenow-app-key']) headers['x-securenow-app-key'] = browserAppKey;
150
140
  if (deploymentEnvironment && !headers['x-securenow-environment']) headers['x-securenow-environment'] = deploymentEnvironment;
151
141
 
152
142
  // ---- naming rules (mirrors tracing.js) ----
153
- const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
143
+ const rawBase = (toConfigString(configValue('app.key')) || toConfigString(configValue('app.name')) || '').trim().replace(/^['"]|['"]$/g, '');
154
144
  const baseName = rawBase || null;
155
145
  // Default to no suffix whenever a baseName is resolved: the dashboard filters
156
146
  // service.name by exact match, and browsers have no PM2 cluster problem
157
147
  // (each tab has its own service.instance.id). Explicit config.runtime.noUuid=false
158
148
  // still re-enables the suffix if someone really wants it.
159
- const noUuidEnv = env('SECURENOW_NO_UUID');
149
+ const noUuidEnv = configValue('config.runtime.noUuid');
160
150
  const noUuid =
161
151
  (noUuidEnv !== undefined && noUuidEnv !== '')
162
152
  ? /^(1|true)$/i.test(String(noUuidEnv).trim())
163
153
  : !!baseName;
164
- const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
154
+ const strict = boolConfig('config.runtime.strict', false);
155
+ const hostedSecureNowIngest = /^https:\/\/ingest\.securenow\.ai(?:\/|$)/i.test(tracesUrl);
165
156
 
166
157
  function uuidv4() {
167
158
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
@@ -195,6 +186,12 @@ if (baseName) {
195
186
  }
196
187
  }
197
188
 
189
+ if (!disabled && hostedSecureNowIngest) {
190
+ disabled = true;
191
+ // eslint-disable-next-line no-console
192
+ console.warn('[securenow/web-vite] Direct browser OTLP ingestion to SecureNow is disabled in v8.0 until browser-safe auth is available. Use the server-side SDK runtime credentials path.');
193
+ }
194
+
198
195
  const instancePrefix = baseName || 'securenow';
199
196
  const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
200
197
 
@@ -203,10 +200,10 @@ try {
203
200
  // eslint-disable-next-line no-console
204
201
  console.log(
205
202
  '[securenow] web preload loaded app.key=%s app.name=%s config.runtime.noUuid=%s config.runtime.strict=%s → service.name=%s instance.id=%s',
206
- JSON.stringify(env('SECURENOW_APPID')),
207
- JSON.stringify(env('OTEL_SERVICE_NAME')),
208
- JSON.stringify(env('SECURENOW_NO_UUID')),
209
- JSON.stringify(env('SECURENOW_STRICT')),
203
+ JSON.stringify(configValue('app.key')),
204
+ JSON.stringify(configValue('app.name')),
205
+ JSON.stringify(configValue('config.runtime.noUuid')),
206
+ JSON.stringify(configValue('config.runtime.strict')),
210
207
  serviceName,
211
208
  serviceInstanceId
212
209
  );
@@ -229,7 +226,7 @@ export function startSecurenowWeb() {
229
226
  [S.SERVICE_NAME]: serviceName,
230
227
  [S.SERVICE_INSTANCE_ID]: serviceInstanceId,
231
228
  [S.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
232
- [S.SERVICE_VERSION]: viteEnv.VITE_APP_VERSION || undefined,
229
+ [S.SERVICE_VERSION]: toConfigString(configValue('app.version')) || undefined,
233
230
  }),
234
231
  spanProcessors: [new BatchSpanProcessor(exporter)],
235
232
  });
@@ -250,8 +247,7 @@ export function startSecurenowWeb() {
250
247
  ],
251
248
  });
252
249
 
253
- // Optional smoke span (same flag name)
254
- if (String(env('SECURENOW_TEST_SPAN')) === '1') {
250
+ if (boolConfig('config.runtime.testSpan', false)) {
255
251
  import('@opentelemetry/api').then(api => {
256
252
  const tracer = api.trace.getTracer('securenow-smoke');
257
253
  const span = tracer.startSpan('securenow.startup.smoke.web'); span.end();
@@ -265,7 +261,7 @@ export function startSecurenowWeb() {
265
261
  // ---- Free trial banner (browser DOM injection) ----
266
262
  function injectFreeTrialBanner() {
267
263
  const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
268
- const hideBanner = String(env('SECURENOW_HIDE_BANNER')) === '1';
264
+ const hideBanner = boolConfig('config.runtime.hideBanner', false);
269
265
  if (hideBanner || !FREE_TRIAL_HOSTS.some(host => endpointBase.includes(host))) return;
270
266
  if (typeof document === 'undefined') return;
271
267