securenow 7.7.15 → 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/SKILL-API.md CHANGED
@@ -336,7 +336,7 @@ const appConfig = require('securenow/app-config');
336
336
 
337
337
  await firewall.init({
338
338
  ...appConfig.resolveFirewallOptions(),
339
- apiUrl: 'https://api.securenow.ai',
339
+ apiUrl: 'https://ingest.securenow.ai',
340
340
  syncInterval: 3600, // safety-net full sync every hour
341
341
  versionCheckInterval: 10, // lightweight ETag check every 10s
342
342
  failMode: 'open', // 'open' or 'closed'
package/app-config.js CHANGED
@@ -18,6 +18,7 @@ const os = require('os');
18
18
 
19
19
  const FREE_TRIAL_INSTANCE = 'https://ingest.securenow.ai';
20
20
  const DEFAULT_API_URL = 'https://api.securenow.ai';
21
+ const DEFAULT_FIREWALL_API_URL = FREE_TRIAL_INSTANCE;
21
22
  const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
22
23
  const LEGACY_ENV_FALLBACK_FLAG = 'SECURENOW_ENABLE_LEGACY_ENV';
23
24
  const CONFIG_SCHEMA_VERSION = 2;
@@ -111,7 +112,7 @@ const CONFIG_EXPLANATIONS = Object.freeze({
111
112
  'config.runtime.testSpan': 'If true, emit a startup smoke span. Prefer `npx securenow test-span` for manual checks.',
112
113
  'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
113
114
  'config.firewall.enabled': 'Secure default: app firewall enforcement starts when apiKey is present and the dashboard toggle is on.',
114
- 'config.firewall.apiUrl': 'SecureNow API base URL for firewall sync.',
115
+ 'config.firewall.apiUrl': 'Optional SecureNow firewall control-plane base URL. Leave unset/default so hosted SDKs sync through the SecureNow ingest gateway.',
115
116
  'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version/ETag checks.',
116
117
  'config.firewall.syncInterval': 'Seconds between safety-net full firewall blocklist syncs.',
117
118
  'config.firewall.failMode': 'open allows traffic if SecureNow is temporarily unreachable; closed blocks all on sync failure.',
@@ -457,6 +458,41 @@ function normalizeSignalEndpoint(value, signalType) {
457
458
  return endpoint;
458
459
  }
459
460
 
461
+ function normalizeFirewallApiUrl(value) {
462
+ const raw = pick(value);
463
+ if (raw == null) return DEFAULT_FIREWALL_API_URL;
464
+ const endpoint = normalizeInstanceEndpoint(raw);
465
+ if (!endpoint) return DEFAULT_FIREWALL_API_URL;
466
+ const trimmed = String(endpoint).trim().replace(/\/$/, '').replace(/\/api(?:\/v1)?$/i, '');
467
+
468
+ // api.securenow.ai was the historical SDK default. Hosted runtime sync now
469
+ // shares the ingest gateway so telemetry and firewall state fail or recover
470
+ // together, and so customer apps need only one public SecureNow egress host.
471
+ if (trimmed === DEFAULT_API_URL || trimmed === LEGACY_SECURENOW_GATEWAY) {
472
+ return DEFAULT_FIREWALL_API_URL;
473
+ }
474
+ if (trimmed === DEFAULT_FIREWALL_API_URL) return DEFAULT_FIREWALL_API_URL;
475
+
476
+ return trimmed;
477
+ }
478
+
479
+ function resolveFirewallApiUrl() {
480
+ return normalizeFirewallApiUrl(configValue('firewall.apiUrl', DEFAULT_API_URL));
481
+ }
482
+
483
+ function resolveFirewallApiFallbacks(primary = resolveFirewallApiUrl()) {
484
+ const normalizedPrimary = normalizeFirewallApiUrl(primary);
485
+ const fallbacks = [];
486
+
487
+ if (normalizedPrimary === DEFAULT_FIREWALL_API_URL) {
488
+ fallbacks.push(DEFAULT_API_URL);
489
+ } else if (normalizedPrimary === DEFAULT_API_URL) {
490
+ fallbacks.push(DEFAULT_FIREWALL_API_URL);
491
+ }
492
+
493
+ return uniq(fallbacks.filter((url) => url && url !== normalizedPrimary));
494
+ }
495
+
460
496
  function pick(value) {
461
497
  if (value === undefined || value === null) return null;
462
498
  if (typeof value === 'string') {
@@ -694,6 +730,10 @@ function resolveOtlpHeaders() {
694
730
  if (appKey && !headers['x-api-key']) {
695
731
  headers['x-api-key'] = appKey;
696
732
  }
733
+ const deploymentEnvironment = resolveDeploymentEnvironment();
734
+ if (deploymentEnvironment && !headers['x-securenow-environment']) {
735
+ headers['x-securenow-environment'] = deploymentEnvironment;
736
+ }
697
737
  return headers;
698
738
  }
699
739
 
@@ -721,12 +761,14 @@ function resolveFirewallEnabled() {
721
761
  }
722
762
 
723
763
  function resolveFirewallOptions() {
764
+ const apiUrl = resolveFirewallApiUrl();
724
765
  return {
725
766
  apiKey: resolveApiKey(),
726
767
  appKey: resolveAppKey() || null,
727
768
  environment: resolveDeploymentEnvironment(),
728
769
  enabled: resolveFirewallEnabled(),
729
- apiUrl: configValue('firewall.apiUrl', DEFAULT_API_URL) || DEFAULT_API_URL,
770
+ apiUrl,
771
+ apiUrlFallbacks: resolveFirewallApiFallbacks(apiUrl),
730
772
  versionCheckInterval: numberConfig('firewall.versionCheckInterval', 10, 1),
731
773
  syncInterval: numberConfig('firewall.syncInterval', 3600, 1),
732
774
  failMode: configValue('firewall.failMode', 'open') || 'open',
@@ -755,6 +797,7 @@ function resolveFirewallOptions() {
755
797
  module.exports = {
756
798
  FREE_TRIAL_INSTANCE,
757
799
  DEFAULT_API_URL,
800
+ DEFAULT_FIREWALL_API_URL,
758
801
  LEGACY_SECURENOW_GATEWAY,
759
802
  LEGACY_ENV_FALLBACK_FLAG,
760
803
  CONFIG_SCHEMA_VERSION,
@@ -783,6 +826,9 @@ module.exports = {
783
826
  resolveOtlpHeaderString,
784
827
  resolveEndpoints,
785
828
  resolveFirewallEnabled,
829
+ normalizeFirewallApiUrl,
830
+ resolveFirewallApiUrl,
831
+ resolveFirewallApiFallbacks,
786
832
  resolveFirewallOptions,
787
833
  env,
788
834
  boolEnv,
@@ -36,6 +36,8 @@ function resolvedConfig(options = {}) {
36
36
  apiKey,
37
37
  firewallLocalEnabled,
38
38
  apiUrl: config.getApiUrl(),
39
+ firewallApiUrl: firewall.apiUrl,
40
+ firewallSyncEndpoint: `${firewall.apiUrl.replace(/\/$/, '')}/api/v1/firewall/sync`,
39
41
  loggingEnabled: appConfig.boolConfig('logging.enabled', true),
40
42
  captureBody: appConfig.boolConfig('capture.body', true),
41
43
  captureMultipart: appConfig.boolConfig('capture.multipart', true),
@@ -330,11 +332,12 @@ async function logSend(args, flags) {
330
332
  }
331
333
  }
332
334
 
333
- function okHttpStatus(status) {
335
+ function okHttpStatus(status, okStatuses) {
336
+ if (Array.isArray(okStatuses) && okStatuses.length) return okStatuses.includes(status);
334
337
  return status >= 200 && status < 300;
335
338
  }
336
339
 
337
- async function probe({ endpoint, method = 'POST', headers = {}, body = null, timeoutMs = 3000 }) {
340
+ async function probe({ endpoint, method = 'POST', headers = {}, body = null, timeoutMs = 3000, okStatuses = null }) {
338
341
  try {
339
342
  const res = await httpRequest({
340
343
  method,
@@ -344,9 +347,9 @@ async function probe({ endpoint, method = 'POST', headers = {}, body = null, tim
344
347
  timeoutMs,
345
348
  });
346
349
  return {
347
- ok: okHttpStatus(res.status),
350
+ ok: okHttpStatus(res.status, okStatuses),
348
351
  status: res.status,
349
- ...(okHttpStatus(res.status) ? {} : { error: `HTTP ${res.status}`, body: res.body ? res.body.slice(0, 500) : '' }),
352
+ ...(okHttpStatus(res.status, okStatuses) ? {} : { error: `HTTP ${res.status}`, body: res.body ? res.body.slice(0, 500) : '' }),
350
353
  };
351
354
  } catch (err) {
352
355
  return { ok: false, error: err.message };
@@ -372,6 +375,8 @@ function env(_args, flags) {
372
375
  otlpHeaders: Object.keys(cfg.headers || {}).length ? '***' : null,
373
376
  firewallApiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
374
377
  apiUrl: cfg.apiUrl,
378
+ firewallApiUrl: cfg.firewallApiUrl,
379
+ firewallSyncEndpoint: cfg.firewallSyncEndpoint,
375
380
  loggingEnabled: cfg.loggingEnabled,
376
381
  otelLogLevel: cfg.otelLogLevel,
377
382
  captureBody: cfg.captureBody,
@@ -397,6 +402,7 @@ function env(_args, flags) {
397
402
  ['Body capture', cfg.captureBody ? ui.c.green('enabled') : ui.c.dim('disabled')],
398
403
  ['Multipart capture', cfg.captureMultipart ? ui.c.green('enabled') : ui.c.dim('disabled')],
399
404
  ['Firewall', firewallStatusLabel(cfg)],
405
+ ['Firewall sync', cfg.firewallEnabled ? cfg.firewallSyncEndpoint : ui.c.dim('(not active)')],
400
406
  ]);
401
407
 
402
408
  ui.heading('Resolved credentials');
@@ -454,6 +460,26 @@ async function doctor(_args, flags) {
454
460
  checks.push({ name: 'api', ...api });
455
461
  }
456
462
 
463
+ if (cfg.apiKey && cfg.firewallLocalEnabled) {
464
+ const query = new URLSearchParams();
465
+ if (cfg.appKey) query.set('app', cfg.appKey);
466
+ if (cfg.deploymentEnvironment) query.set('env', cfg.deploymentEnvironment);
467
+ const endpoint = `${cfg.firewallSyncEndpoint}${query.toString() ? `?${query.toString()}` : ''}`;
468
+ const spin4 = ui.spinner(`Probing firewall sync ${endpoint}`);
469
+ const sync = await probe({
470
+ method: 'GET',
471
+ endpoint,
472
+ headers: {
473
+ Authorization: `Bearer ${cfg.apiKey}`,
474
+ 'X-SecureNow-Environment': cfg.deploymentEnvironment,
475
+ },
476
+ okStatuses: [200, 304],
477
+ });
478
+ if (sync.ok) spin4.stop(`Firewall sync reachable (HTTP ${sync.status})`);
479
+ else spin4.fail(`Firewall sync unreachable: ${sync.error}`);
480
+ checks.push({ name: 'firewall-sync', ...sync });
481
+ }
482
+
457
483
  const warnings = [];
458
484
  const singletonOkMessage = otelApiCheck.ok && otelApiCheck.packages.length
459
485
  ? `OpenTelemetry API singleton OK (${otelApiCheck.versions[0] || 'unknown'})`
package/firewall-only.js CHANGED
@@ -26,6 +26,7 @@ if (firewallOptions.apiKey && firewallOptions.enabled) {
26
26
  appKey: firewallOptions.appKey,
27
27
  environment: firewallOptions.environment,
28
28
  apiUrl: firewallOptions.apiUrl,
29
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
29
30
  versionCheckInterval: firewallOptions.versionCheckInterval,
30
31
  syncInterval: firewallOptions.syncInterval,
31
32
  failMode: firewallOptions.failMode,
package/firewall.js CHANGED
@@ -21,6 +21,7 @@ let _stats = { syncs: 0, blocked: 0, rateLimited: 0, allowed: 0, versionChecks:
21
21
  let _localhostFallbackTried = false;
22
22
  let _eventQueue = [];
23
23
  let _eventTimer = null;
24
+ let _remainingApiUrlFallbacks = [];
24
25
 
25
26
  // Remote toggle - set by /firewall/sync when an appKey is in scope. Default
26
27
  // true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
@@ -60,7 +61,7 @@ const _httpsAgent = new https.Agent({ keepAlive: false });
60
61
  const EVENT_FLUSH_INTERVAL_MS = 2_000;
61
62
  const EVENT_BATCH_SIZE = 25;
62
63
  const EVENT_QUEUE_MAX = 1_000;
63
- const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
64
+ const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND']);
64
65
 
65
66
  // Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
66
67
  let _useUnifiedSync = true;
@@ -156,7 +157,13 @@ function agentFor(url) {
156
157
  function isTransientNetworkError(err) {
157
158
  if (!err) return false;
158
159
  if (err.code && TRANSIENT_NETWORK_CODES.has(err.code)) return true;
159
- return /socket hang up|connection reset|ECONNRESET/i.test(String(err.message || ''));
160
+ return /socket hang up|connection reset|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(String(err.message || ''));
161
+ }
162
+
163
+ function isApiReachabilityError(err) {
164
+ if (isTransientNetworkError(err)) return true;
165
+ const text = `${err && err.code || ''} ${err && err.message || ''}`;
166
+ return /TLS|SSL|certificate|CERT_|UNABLE_TO_VERIFY|self signed/i.test(text);
160
167
  }
161
168
 
162
169
  function formatRequestError(err) {
@@ -167,6 +174,33 @@ function formatRequestError(err) {
167
174
  return parts.join(' ');
168
175
  }
169
176
 
177
+ function resetApiUrlFallbacks() {
178
+ const seen = new Set([_options && _options.apiUrl].filter(Boolean));
179
+ _remainingApiUrlFallbacks = [];
180
+ for (const candidate of Array.isArray(_options && _options.apiUrlFallbacks) ? _options.apiUrlFallbacks : []) {
181
+ const url = String(candidate || '').trim().replace(/\/$/, '');
182
+ if (!url || seen.has(url)) continue;
183
+ seen.add(url);
184
+ _remainingApiUrlFallbacks.push(url);
185
+ }
186
+ }
187
+
188
+ function switchToNextApiUrl(reason) {
189
+ if (!_options || _remainingApiUrlFallbacks.length === 0) return false;
190
+ const previous = _options.apiUrl;
191
+ _options.apiUrl = _remainingApiUrlFallbacks.shift();
192
+ _lastUnifiedEtag = null;
193
+ _lastSyncEtag = null;
194
+ _lastAllowlistSyncEtag = null;
195
+ if (_options.log) {
196
+ fwWarn('[securenow] Firewall: %s unreachable (%s), retrying sync via %s',
197
+ previous,
198
+ reason || 'network error',
199
+ _options.apiUrl);
200
+ }
201
+ return true;
202
+ }
203
+
170
204
  function requestOnce(method, url, body, extraHeaders, timeout, callback) {
171
205
  const mod = url.startsWith('https') ? https : http;
172
206
  const parsed = new URL(url);
@@ -562,9 +596,15 @@ function pollOnce(callback) {
562
596
 
563
597
  const done = (err, result) => {
564
598
  _pollInflight = false;
565
- if (err) {
566
- _consecutiveErrors++;
567
- _stats.errors++;
599
+ if (err) {
600
+ if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
601
+ _pollInflight = false;
602
+ const retryTimer = setTimeout(() => pollOnce(callback), 1000);
603
+ if (retryTimer.unref) retryTimer.unref();
604
+ return;
605
+ }
606
+ _consecutiveErrors++;
607
+ _stats.errors++;
568
608
  maybeOpenCircuit();
569
609
  if (_options.log) fwWarn('[securenow] Firewall: poll failed:', formatRequestError(err));
570
610
  return callback(err);
@@ -623,9 +663,14 @@ function startSyncLoop() {
623
663
  const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
624
664
 
625
665
  syncFn((err, result) => {
626
- if (err) {
627
- const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
628
- if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
666
+ if (err) {
667
+ const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
668
+ if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
669
+ const retryTimer = setTimeout(initialSync, 1000);
670
+ if (retryTimer.unref) retryTimer.unref();
671
+ return;
672
+ }
673
+ if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
629
674
  _localhostFallbackTried = true;
630
675
  const origUrl = _options.apiUrl;
631
676
  _options.apiUrl = 'http://localhost:4000';
@@ -981,10 +1026,15 @@ function patchHttpLayer() {
981
1026
 
982
1027
  // Init
983
1028
 
984
- function init(options) {
985
- _options = options;
1029
+ function init(options) {
1030
+ _options = options || {};
1031
+ _options.apiUrl = String(_options.apiUrl || '').trim().replace(/\/$/, '');
1032
+ _localhostFallbackTried = false;
1033
+ _useUnifiedSync = true;
1034
+ resetApiUrlFallbacks();
986
1035
 
987
1036
  if (_options.log) fwLog('[securenow] Firewall: ENABLED');
1037
+ if (_options.log && _options.apiUrl) fwLog('[securenow] Firewall: sync endpoint=%s/api/v1/firewall/sync', _options.apiUrl);
988
1038
  if (_options.log && _options.environment) fwLog('[securenow] Firewall: environment=%s', _options.environment);
989
1039
 
990
1040
  patchHttpLayer();
@@ -1043,8 +1093,10 @@ function shutdown() {
1043
1093
  _consecutiveErrors = 0;
1044
1094
  _pollInflight = false;
1045
1095
  _retryAfterUntil = 0;
1096
+ _localhostFallbackTried = false;
1046
1097
  _rateLimitRules = [];
1047
1098
  _rateLimitBuckets = new Map();
1099
+ _remainingApiUrlFallbacks = [];
1048
1100
 
1049
1101
  _httpAgent.destroy();
1050
1102
  _httpsAgent.destroy();
package/nextjs.js CHANGED
@@ -189,6 +189,8 @@ function registerSecureNow(options = {}) {
189
189
  if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
190
190
  if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
191
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;
192
194
  }
193
195
 
194
196
  console.log('[securenow] Next.js App -> service.name=%s', serviceName);
@@ -376,11 +378,11 @@ function registerSecureNow(options = {}) {
376
378
  // the remote end (ECONNRESET / "socket hang up"). These transient errors
377
379
  // sometimes escape as unhandled exceptions or rejections. We catch them
378
380
  // here and log at debug level instead of crashing the host app.
379
- const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
381
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
380
382
  function _isOtlpTransientError(err) {
381
383
  if (!err) return false;
382
384
  if (_TRANSIENT_CODES.has(err.code)) return true;
383
- if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
385
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
384
386
  return false;
385
387
  }
386
388
  function _looksLikeOtlpStack(err) {
@@ -389,21 +391,53 @@ function registerSecureNow(options = {}) {
389
391
  return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
390
392
  || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
391
393
  }
394
+ function _looksLikeConfiguredOtlpEndpoint(err) {
395
+ const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
396
+ try {
397
+ const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
398
+ return hosts.some((host) => host && text.includes(host));
399
+ } catch (_) {
400
+ return false;
401
+ }
402
+ }
392
403
  const _diagDebug = otelLogLevel === 'debug';
404
+ let _lastSuppressedOtlpErrorLogAt = 0;
405
+ function _originalConsole(method) {
406
+ const originals = console.__securenow_original || console.__securenowOriginalConsole;
407
+ return (originals && originals[method]) || console[method] || console.log;
408
+ }
409
+ function _formatOtlpError(err) {
410
+ if (!err) return 'unknown error';
411
+ const parts = [err.message || String(err)];
412
+ if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
413
+ if (err.syscall) parts.push(`syscall=${err.syscall}`);
414
+ if (err.hostname) parts.push(`host=${err.hostname}`);
415
+ return parts.join(' ');
416
+ }
417
+ function _reportSuppressedOtlpError(kind, err, origin) {
418
+ if (otelLogLevel === 'none') return;
419
+ const now = Date.now();
420
+ if (!_diagDebug && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
421
+ _lastSuppressedOtlpErrorLogAt = now;
422
+ const method = _diagDebug ? 'debug' : 'error';
423
+ _originalConsole(method).call(
424
+ console,
425
+ '[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
426
+ kind,
427
+ origin || 'async',
428
+ _formatOtlpError(err)
429
+ );
430
+ }
393
431
  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
- }
432
+ if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
433
+ _reportSuppressedOtlpError('error', err, origin);
398
434
  return;
399
435
  }
400
436
  throw err;
401
437
  });
402
438
  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
- }
439
+ if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
440
+ _reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
407
441
  return;
408
442
  }
409
443
  throw reason;
@@ -632,6 +666,7 @@ function registerSecureNow(options = {}) {
632
666
  appKey: firewallOptions.appKey,
633
667
  environment: deploymentEnvironment || firewallOptions.environment,
634
668
  apiUrl: firewallOptions.apiUrl,
669
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
635
670
  versionCheckInterval: firewallOptions.versionCheckInterval,
636
671
  syncInterval: firewallOptions.syncInterval,
637
672
  failMode: firewallOptions.failMode,
@@ -326,6 +326,7 @@ export default defineNitroPlugin(async (nitroApp) => {
326
326
  appKey: firewallOptions.appKey,
327
327
  environment: deploymentEnvironment || firewallOptions.environment,
328
328
  apiUrl: firewallOptions.apiUrl,
329
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
329
330
  versionCheckInterval: firewallOptions.versionCheckInterval,
330
331
  syncInterval: firewallOptions.syncInterval,
331
332
  failMode: firewallOptions.failMode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.15",
3
+ "version": "7.7.16",
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
@@ -580,11 +580,11 @@ if (loggingEnabled) {
580
580
  // request's error path isn't fully covered by the OTel library. We install
581
581
  // targeted process-level handlers to catch them and log at debug level instead
582
582
  // of crashing the host app.
583
- const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
583
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
584
584
  function _isOtlpTransientError(err) {
585
585
  if (!err) return false;
586
586
  if (_TRANSIENT_CODES.has(err.code)) return true;
587
- 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;
588
588
  return false;
589
589
  }
590
590
  function _looksLikeOtlpStack(err) {
@@ -593,22 +593,55 @@ function _looksLikeOtlpStack(err) {
593
593
  return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
594
594
  || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
595
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
+ }
596
633
 
597
634
  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
635
+ if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
636
+ _reportSuppressedOtlpError('error', err, origin);
637
+ return; // swallow - do not crash
603
638
  }
604
639
  // Not ours — re-throw so the default handler (or the app's own handler) fires
605
640
  throw err;
606
641
  });
607
642
  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
- }
643
+ if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
644
+ _reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
612
645
  return; // swallow
613
646
  }
614
647
  // Not ours — re-throw as unhandled so Node's default behaviour applies
@@ -669,6 +702,7 @@ const sdk = new NodeSDK({
669
702
  appKey: firewallOptions.appKey,
670
703
  environment: firewallOptions.environment,
671
704
  apiUrl: firewallOptions.apiUrl,
705
+ apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
672
706
  versionCheckInterval: firewallOptions.versionCheckInterval,
673
707
  syncInterval: firewallOptions.syncInterval,
674
708
  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, '');