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 +1 -1
- package/app-config.js +48 -2
- package/cli/diagnostics.js +30 -4
- package/firewall-only.js +1 -0
- package/firewall.js +62 -10
- package/nextjs.js +45 -10
- package/nuxt-server-plugin.mjs +1 -0
- package/package.json +1 -1
- package/tracing.js +45 -11
- package/web-vite.mjs +3 -0
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://
|
|
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
|
|
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
|
|
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,
|
package/cli/diagnostics.js
CHANGED
|
@@ -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
|
-
|
|
567
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -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
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
|
|
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
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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, '');
|