securenow 7.2.0 → 7.3.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/app-config.js CHANGED
@@ -157,6 +157,30 @@ function resolveAll() {
157
157
  };
158
158
  }
159
159
 
160
+ // Decide whether to append a per-worker UUID suffix to service.name.
161
+ //
162
+ // The dashboard filters traces with an exact match on service.name
163
+ // (`resource_string_service$$name IN (...)`), so when appId IS the routing
164
+ // UUID (customer is logged in → credentials file provides app.key), the
165
+ // suffix guarantees a miss. Per-worker disambiguation still happens via
166
+ // service.instance.id, which is never used for filtering.
167
+ //
168
+ // Precedence:
169
+ // 1. Explicit opts.noUuid (caller passed it) — wins
170
+ // 2. Explicit SECURENOW_NO_UUID env var (0/1/true/false) — wins
171
+ // 3. appKey resolved (logged-in, appId is routing UUID) → true
172
+ // 4. Otherwise (pre-login, appId = package.json#name) → false
173
+ function resolveNoUuid(opts = {}) {
174
+ if (opts.noUuid !== undefined && opts.noUuid !== null) return !!opts.noUuid;
175
+
176
+ const raw = process.env.SECURENOW_NO_UUID;
177
+ if (raw !== undefined && raw !== '') {
178
+ return /^(1|true)$/i.test(String(raw).trim());
179
+ }
180
+
181
+ return !!resolveAppKey();
182
+ }
183
+
160
184
  module.exports = {
161
185
  FREE_TRIAL_INSTANCE,
162
186
  resolveAppKey,
@@ -165,6 +189,7 @@ module.exports = {
165
189
  resolveApiKey,
166
190
  resolveInstance,
167
191
  resolveAll,
192
+ resolveNoUuid,
168
193
  loadCredentials,
169
194
  loadLocalCredentials,
170
195
  loadGlobalCredentials,
@@ -8,12 +8,20 @@ const config = require('./config');
8
8
  // ── Config resolution (mirrors tracing.js priority order) ──
9
9
 
10
10
  function resolvedConfig() {
11
- const serviceName =
12
- process.env.OTEL_SERVICE_NAME ||
13
- process.env.SECURENOW_APPID ||
14
- '(auto-generated)';
11
+ // Mirror the SDK's resolution: env → credentials file → package.json#name.
12
+ // Doing it here means `securenow doctor` reports what the SDK will actually
13
+ // send — critical for diagnosing "my traces don't show up" (the common
14
+ // cause is a service.name mismatch with the dashboard's exact-match filter).
15
+ const appConfig = require('../app-config');
16
+ const resolvedApp = appConfig.resolveAll();
17
+ const noUuid = appConfig.resolveNoUuid();
18
+
19
+ const baseName = (process.env.OTEL_SERVICE_NAME || resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '') || null;
20
+ const serviceName = baseName
21
+ ? (noUuid ? baseName : `${baseName}-<uuid-per-worker>`)
22
+ : '(auto-generated)';
15
23
  const instance =
16
- process.env.SECURENOW_INSTANCE || 'https://freetrial.securenow.ai:4318';
24
+ resolvedApp.instance || 'https://freetrial.securenow.ai:4318';
17
25
  const tracesEndpoint =
18
26
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
19
27
  (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -297,7 +305,7 @@ function env(_args, flags) {
297
305
  SECURENOW_API_URL: cfg.apiUrl,
298
306
  SECURENOW_LOGGING_ENABLED: cfg.loggingEnabled ? '1' : '0',
299
307
  SECURENOW_CAPTURE_BODY: cfg.captureBody ? '1' : '0',
300
- SECURENOW_NO_UUID: process.env.SECURENOW_NO_UUID || '0',
308
+ SECURENOW_NO_UUID: process.env.SECURENOW_NO_UUID || `(auto: ${require('../app-config').resolveNoUuid() ? '1' : '0'})`,
301
309
  SECURENOW_FIREWALL_TCP: process.env.SECURENOW_FIREWALL_TCP || '0',
302
310
  SECURENOW_FIREWALL_IPTABLES: process.env.SECURENOW_FIREWALL_IPTABLES || '0',
303
311
  SECURENOW_FIREWALL_CLOUD: process.env.SECURENOW_FIREWALL_CLOUD || null,
package/cli/firewall.js CHANGED
@@ -97,4 +97,78 @@ async function testIp(args, flags) {
97
97
  }
98
98
  }
99
99
 
100
- module.exports = { status, testIp };
100
+ async function resolveAppKey(flags) {
101
+ if (flags && flags.app) return String(flags.app);
102
+ try {
103
+ const { resolveAppKey: r } = require('../app-config');
104
+ const k = r();
105
+ if (k) return k;
106
+ } catch (_) {}
107
+ return null;
108
+ }
109
+
110
+ async function setEnabled(args, flags, enabled) {
111
+ requireAuth();
112
+ const appKey = await resolveAppKey(flags);
113
+ if (!appKey) {
114
+ ui.error('No app selected. Pass --app <key> or run `securenow login` / `securenow apps default <key>`.');
115
+ process.exit(1);
116
+ }
117
+
118
+ const verb = enabled ? 'Enabling' : 'Disabling';
119
+ const s = ui.spinner(`${verb} firewall for ${appKey}`);
120
+ try {
121
+ const data = await api.patch(`/firewall/app/${encodeURIComponent(appKey)}`, { enabled });
122
+ s.stop(`Firewall ${enabled ? 'enabled' : 'disabled'}`);
123
+
124
+ if (flags.json) { ui.json(data); return; }
125
+
126
+ const app = data.app || {};
127
+ console.log('');
128
+ console.log(` ${ui.c.bold(enabled ? ui.c.green('Firewall: ENABLED') : ui.c.yellow('Firewall: DISABLED'))} — ${app.name || appKey}`);
129
+ console.log(` ${ui.c.dim('App key:')} ${app.key || appKey}`);
130
+ console.log('');
131
+ console.log(` ${ui.c.dim('Running SDK instances pick up the change within ~10s.')}`);
132
+ console.log('');
133
+ } catch (err) {
134
+ s.fail(`Failed to ${enabled ? 'enable' : 'disable'} firewall`);
135
+ throw err;
136
+ }
137
+ }
138
+
139
+ async function enable(args, flags) { return setEnabled(args, flags, true); }
140
+ async function disable(args, flags) { return setEnabled(args, flags, false); }
141
+
142
+ async function appsList(args, flags) {
143
+ requireAuth();
144
+ const s = ui.spinner('Loading apps');
145
+ try {
146
+ const data = await api.get('/firewall/apps');
147
+ s.stop('Apps loaded');
148
+
149
+ if (flags.json) { ui.json(data); return; }
150
+
151
+ const apps = data.apps || [];
152
+ if (apps.length === 0) {
153
+ console.log('');
154
+ console.log(` ${ui.c.dim('No apps yet. Create one with `securenow apps create <name>`.')}`);
155
+ console.log('');
156
+ return;
157
+ }
158
+
159
+ console.log('');
160
+ for (const a of apps) {
161
+ const tag = a.firewallEnabled ? ui.c.green('ON ') : ui.c.yellow('OFF');
162
+ console.log(` ${tag} ${ui.c.bold(a.name)} ${ui.c.dim(a.key)}`);
163
+ }
164
+ console.log('');
165
+ console.log(` ${ui.c.dim('Toggle:')} securenow firewall enable --app <key>`);
166
+ console.log(` ${ui.c.dim(' :')} securenow firewall disable --app <key>`);
167
+ console.log('');
168
+ } catch (err) {
169
+ s.fail('Failed to list apps');
170
+ throw err;
171
+ }
172
+ }
173
+
174
+ module.exports = { status, testIp, enable, disable, appsList };
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
4
  const ui = require('./cli/ui');
@@ -178,10 +178,14 @@ const COMMANDS = {
178
178
  defaultSub: 'list',
179
179
  },
180
180
  firewall: {
181
- desc: 'Firewall status and IP testing',
181
+ desc: 'Firewall status, per-app toggle, and IP testing',
182
182
  usage: 'securenow firewall <subcommand> [options]',
183
+ flags: { app: 'App key (defaults to logged-in app)', json: 'Output as JSON' },
183
184
  sub: {
184
185
  status: { desc: 'Show firewall status, layers, and blocklist info', run: (a, f) => require('./cli/firewall').status(a, f) },
186
+ apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
187
+ enable: { desc: 'Turn the firewall ON for an app', usage: 'securenow firewall enable [--app <key>]', flags: { app: 'App key (defaults to logged-in app)' }, run: (a, f) => require('./cli/firewall').enable(a, f) },
188
+ disable: { desc: 'Turn the firewall OFF for an app', usage: 'securenow firewall disable [--app <key>]', flags: { app: 'App key (defaults to logged-in app)' }, run: (a, f) => require('./cli/firewall').disable(a, f) },
185
189
  'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip>', run: (a, f) => require('./cli/firewall').testIp(a, f) },
186
190
  },
187
191
  defaultSub: 'status',
@@ -25,7 +25,7 @@ Complete reference for all environment variables supported by SecureNow.
25
25
  | **SECURENOW_DISABLE_INSTRUMENTATIONS** | Optional | - | Comma-separated list of OTel instrumentations to disable |
26
26
  | **SECURENOW_TEST_SPAN** | Optional | `0` | Emit a single test span on startup (prefer `npx securenow test-span`) |
27
27
  | **SECURENOW_HIDE_BANNER** | Optional | `0` | Hide the free-trial banner |
28
- | **SECURENOW_FIREWALL_ENABLED** | Optional | `1` (on when API key is set) | Firewall master switch |
28
+ | **SECURENOW_FIREWALL_ENABLED** | Optional | (dashboard toggle) | Local override only — set to `0` to force-off regardless of dashboard. Primary control is the per-app toggle at `/dashboard/firewall` (≥ 7.3.0). |
29
29
  | **SECURENOW_ENABLE_MONGODB_INSTRUMENTATION** | Optional | `0` | Opt in to MongoDB instrumentation (off by default since a cursor bug on mongodb@6.6+; safe since SDK v6.0.2) |
30
30
  | **OTEL_SERVICE_NAME** | Optional | - | Alternative to SECURENOW_APPID (label only, no routing) |
31
31
  | **OTEL_EXPORTER_OTLP_ENDPOINT** | Optional | - | Alternative to SECURENOW_INSTANCE |
package/firewall-only.js CHANGED
@@ -16,12 +16,17 @@ try { require('dotenv').config(); } catch (_) {}
16
16
  const env = (k) =>
17
17
  process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
18
18
 
19
- const { resolveApiKey } = require('./app-config');
19
+ const { resolveApiKey, resolveAppKey } = require('./app-config');
20
20
  const firewallApiKey = resolveApiKey();
21
+ const appKey = resolveAppKey();
21
22
 
23
+ // SECURENOW_FIREWALL_ENABLED=0 is a hard local override (ops escape hatch).
24
+ // In all other cases the toggle lives in the dashboard; default ON when an
25
+ // API key is present.
22
26
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
23
27
  require('./firewall').init({
24
28
  apiKey: firewallApiKey,
29
+ appKey: appKey || null,
25
30
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
26
31
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
27
32
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
package/firewall.js CHANGED
@@ -16,9 +16,16 @@ let _initialized = false;
16
16
  let _consecutiveErrors = 0;
17
17
  let _layers = [];
18
18
  let _rawIps = [];
19
- let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
19
+ let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0, suppressedDisabled: 0 };
20
20
  let _localhostFallbackTried = false;
21
21
 
22
+ // Remote toggle — set by /firewall/sync when an appKey is in scope. Default
23
+ // true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
24
+ // When the dashboard / CLI flips this off, the next poll suppresses
25
+ // enforcement without restarting the host process.
26
+ let _remoteEnabled = true;
27
+ let _lastRemoteEnabled = null;
28
+
22
29
  // Allowlist state
23
30
  let _allowlistMatcher = null;
24
31
  let _allowlistRawIps = [];
@@ -133,7 +140,8 @@ function httpGet(url, extraHeaders, timeout, callback) {
133
140
  // ────── Unified Sync (v2 — single request for everything) ──────
134
141
 
135
142
  function doUnifiedSync(callback) {
136
- const url = buildUrl(_options.apiUrl, '/firewall/sync');
143
+ const appQuery = _options.appKey ? `?app=${encodeURIComponent(_options.appKey)}` : '';
144
+ const url = buildUrl(_options.apiUrl, '/firewall/sync') + appQuery;
137
145
  const headers = {};
138
146
  if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
139
147
  if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
@@ -165,6 +173,21 @@ function doUnifiedSync(callback) {
165
173
  let blChanged = false;
166
174
  let alChanged = false;
167
175
 
176
+ // Apply remote per-app toggle. Absent body.app means the backend either
177
+ // doesn't know about appKey-scoped sync (older API) or no appKey was
178
+ // sent — leave the previous value untouched (default true on first run).
179
+ if (body.app && typeof body.app.firewallEnabled === 'boolean') {
180
+ const next = body.app.firewallEnabled;
181
+ if (next !== _lastRemoteEnabled) {
182
+ _remoteEnabled = next;
183
+ if (_options.log) {
184
+ console.log('[securenow] Firewall: remote toggle → %s (app=%s)',
185
+ next ? 'ENABLED' : 'DISABLED', body.app.key || _options.appKey);
186
+ }
187
+ _lastRemoteEnabled = next;
188
+ }
189
+ }
190
+
168
191
  // Update blocklist version + data
169
192
  if (body.blocklist) {
170
193
  const newVer = body.blocklist.version;
@@ -559,6 +582,14 @@ function sendBlockResponse(req, res, ip) {
559
582
  }
560
583
 
561
584
  function firewallRequestHandler(req, res) {
585
+ // Remote disable wins over everything: when the dashboard / CLI flips the
586
+ // toggle off, requests pass through the SDK as if the firewall weren't
587
+ // installed. The poll loop keeps running so we re-enable within seconds.
588
+ if (_remoteEnabled === false) {
589
+ _stats.suppressedDisabled++;
590
+ return false;
591
+ }
592
+
562
593
  const ip = resolveClientIp(req);
563
594
 
564
595
  // Allowlist check: if active, only listed IPs are allowed through
@@ -711,10 +742,16 @@ function getStats() {
711
742
  circuitState: _circuitState,
712
743
  consecutiveErrors: _consecutiveErrors,
713
744
  unifiedSync: _useUnifiedSync,
745
+ remoteEnabled: _remoteEnabled,
746
+ appKey: _options ? _options.appKey || null : null,
714
747
  };
715
748
  }
716
749
 
717
- function getMatcher() { return _matcher; }
718
- function getAllowlistMatcher() { return _allowlistMatcher; }
750
+ // Layers (TCP / iptables / cloud) read the matcher to populate kernel-level
751
+ // rules. When the remote toggle is off, return null so they treat the policy
752
+ // as "no IPs to block" without us mutating the cached matcher.
753
+ function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
754
+ function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
755
+ function isRemoteEnabled() { return _remoteEnabled !== false; }
719
756
 
720
- module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher };
757
+ module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, isRemoteEnabled };
package/nextjs.js CHANGED
@@ -150,7 +150,10 @@ function registerSecureNow(options = {}) {
150
150
 
151
151
  const rawBase = (options.serviceName || resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
152
152
  const baseName = rawBase || null;
153
- const noUuid = options.noUuid ?? (String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
153
+ // Default: auto-disable suffix when logged in (appId is the routing UUID
154
+ // and the dashboard does exact match). opts.noUuid or SECURENOW_NO_UUID
155
+ // override.
156
+ const noUuid = appConfig.resolveNoUuid({ noUuid: options.noUuid });
154
157
 
155
158
  // service.name
156
159
  let serviceName;
@@ -613,11 +616,14 @@ function registerSecureNow(options = {}) {
613
616
  // Firewall — runs independently from OTel so it works even if tracing fails.
614
617
  // Key comes from env OR .securenow/credentials.json (set via
615
618
  // `npx securenow api-key set snk_live_...`), so you don't need a .env entry.
616
- const firewallApiKey = require('./app-config').resolveApiKey();
619
+ const appConfig = require('./app-config');
620
+ const firewallApiKey = appConfig.resolveApiKey();
621
+ const firewallAppKey = appConfig.resolveAppKey();
617
622
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
618
623
  try {
619
624
  require('./firewall').init({
620
625
  apiKey: firewallApiKey,
626
+ appKey: firewallAppKey || null,
621
627
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
622
628
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
623
629
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
@@ -83,10 +83,10 @@ export default defineNitroPlugin((nitroApp) => {
83
83
  // ── Naming ──
84
84
  const rawBase = (opts.serviceName || resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
85
85
  const baseName = rawBase || null;
86
- const noUuid =
87
- opts.noUuid ??
88
- (String(env('SECURENOW_NO_UUID')) === '1' ||
89
- String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true');
86
+ // Default: auto-disable per-worker suffix when logged in (appId is the
87
+ // routing UUID and the dashboard does exact match). opts.noUuid or
88
+ // SECURENOW_NO_UUID override.
89
+ const noUuid = appConfig.resolveNoUuid({ noUuid: opts.noUuid });
90
90
 
91
91
  let serviceName;
92
92
  if (baseName) {
@@ -328,11 +328,13 @@ export default defineNitroPlugin((nitroApp) => {
328
328
 
329
329
  // ── Firewall — runs independently from OTel ──
330
330
  const firewallApiKey = env('SECURENOW_API_KEY');
331
+ const firewallAppKey = env('SECURENOW_APPID') || null;
331
332
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
332
333
  try {
333
334
  const { init: fwInit } = await import('./firewall.js');
334
335
  fwInit({
335
336
  apiKey: firewallApiKey,
337
+ appKey: firewallAppKey,
336
338
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
337
339
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
338
340
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.2.0",
3
+ "version": "7.3.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",
package/tracing.js CHANGED
@@ -10,7 +10,11 @@
10
10
  *
11
11
  * Env:
12
12
  * SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
13
- * SECURENOW_NO_UUID=1 # one service.name across all workers
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).
14
18
  * SECURENOW_INSTANCE=http://host:4318 # OTLP/HTTP base (default https://freetrial.securenow.ai:4318)
15
19
  * OTEL_EXPORTER_OTLP_ENDPOINT=... # alternative base
16
20
  * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
@@ -288,7 +292,10 @@ const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
288
292
  // -------- naming rules --------
289
293
  const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
290
294
  const baseName = rawBase || null;
291
- const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
295
+ // Auto-disables the per-worker suffix when we resolved a routing UUID from
296
+ // credentials — the dashboard does exact-match IN on service.name, so any
297
+ // suffix breaks routing. Env SECURENOW_NO_UUID=0|1 still overrides.
298
+ const noUuid = appConfig.resolveNoUuid();
292
299
  const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
293
300
  const inPm2Cluster = !!(process.env.NODE_APP_INSTANCE || process.env.pm_id);
294
301
 
@@ -604,11 +611,15 @@ const sdk = new NodeSDK({
604
611
  patchHttpForBanner();
605
612
  }
606
613
 
607
- // Firewall — auto-activates when SECURENOW_API_KEY is set
608
- const firewallApiKey = env('SECURENOW_API_KEY');
614
+ // Firewall — auto-activates only when a real snk_live_ key is resolvable.
615
+ // resolveApiKey() enforces the prefix, so we skip cleanly when the app has
616
+ // only an app-routing UUID (or nothing at all) — no 401 polling loops.
617
+ const firewallApiKey = appConfig.resolveApiKey();
618
+ const firewallAppKey = appConfig.resolveAppKey();
609
619
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
610
620
  require('./firewall').init({
611
621
  apiKey: firewallApiKey,
622
+ appKey: firewallAppKey || null,
612
623
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
613
624
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
614
625
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
package/web-vite.mjs CHANGED
@@ -52,7 +52,15 @@ const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
52
52
  // ---- naming rules (mirrors tracing.js) ----
53
53
  const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
54
54
  const baseName = rawBase || null;
55
- const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
55
+ // Default to no suffix whenever a baseName is resolved: the dashboard filters
56
+ // service.name by exact match, and browsers have no PM2 cluster problem
57
+ // (each tab has its own service.instance.id). Explicit SECURENOW_NO_UUID=0
58
+ // still re-enables the suffix if someone really wants it.
59
+ const noUuidEnv = env('SECURENOW_NO_UUID');
60
+ const noUuid =
61
+ (noUuidEnv !== undefined && noUuidEnv !== '')
62
+ ? /^(1|true)$/i.test(String(noUuidEnv).trim())
63
+ : !!baseName;
56
64
  const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
57
65
 
58
66
  function uuidv4(): string {