securenow 7.7.15 → 7.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +35 -22
- package/README.md +50 -32
- package/SKILL-API.md +49 -25
- package/SKILL-CLI.md +61 -40
- package/app-config.js +127 -18
- package/cli/apiKey.js +7 -7
- package/cli/apps.js +3 -3
- package/cli/auth.js +113 -31
- package/cli/client.js +14 -13
- package/cli/config.js +219 -45
- package/cli/credentials.js +3 -3
- package/cli/diagnostics.js +35 -10
- package/cli/firewall.js +19 -7
- package/cli/init.js +5 -5
- package/cli/security.js +31 -11
- package/cli.js +57 -22
- package/firewall-only.js +4 -4
- package/firewall.js +172 -45
- package/mcp/catalog.js +43 -30
- package/mcp/server.js +73 -12
- package/nextjs.js +49 -11
- package/nuxt-server-plugin.mjs +8 -4
- package/otel-defaults.js +11 -0
- package/package.json +2 -1
- package/tracing.js +49 -12
- package/web-vite.mjs +3 -0
package/mcp/server.js
CHANGED
|
@@ -43,29 +43,83 @@ function fail(id, code, message, data) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
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 =
|
|
57
|
-
? '
|
|
97
|
+
const runtimeFirewallWarning = app && !apiKey
|
|
98
|
+
? 'Runtime app is connected, but the firewall key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` to refresh .securenow/runtime.json.'
|
|
58
99
|
: null;
|
|
59
100
|
return {
|
|
60
101
|
authenticated: hasBearer,
|
|
102
|
+
adminAuthenticated: !!token,
|
|
103
|
+
runtimeConnected: !!app?.key,
|
|
61
104
|
sessionTokenAvailable: !!token,
|
|
62
105
|
apiKeyAvailable: !!apiKey,
|
|
63
106
|
runtimeFirewallKeyAvailable: !!apiKey,
|
|
64
|
-
|
|
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
|
|
82
|
-
: 'Run `npx securenow login`
|
|
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
|
|
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
|
-
|
|
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,
|
|
163
|
+
const request = buildApiRequest(tool, finalArgs);
|
|
103
164
|
const options = {
|
|
104
165
|
token,
|
|
105
166
|
...(Object.keys(request.query || {}).length > 0 ? { query: request.query } : {}),
|
|
@@ -197,7 +258,7 @@ async function handleRequest(message) {
|
|
|
197
258
|
const result = await callApiTool(tool, args);
|
|
198
259
|
return ok(id, asToolResult({
|
|
199
260
|
tool: name,
|
|
200
|
-
arguments: sanitizeArgs(args),
|
|
261
|
+
arguments: sanitizeArgs(withRuntimeAppDefaults(tool, args)),
|
|
201
262
|
result,
|
|
202
263
|
}));
|
|
203
264
|
}
|
package/nextjs.js
CHANGED
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
const { randomUUID } = require('crypto');
|
|
34
|
+
const { defaultMetricsExporterToNone } = require('./otel-defaults');
|
|
34
35
|
const appConfig = require('./app-config');
|
|
35
36
|
const { resolveClientIpWithDetails } = require('./resolve-ip');
|
|
36
37
|
const otelResources = require('@opentelemetry/resources');
|
|
37
38
|
|
|
39
|
+
defaultMetricsExporterToNone();
|
|
40
|
+
|
|
38
41
|
let isRegistered = false;
|
|
39
42
|
|
|
40
43
|
function requireRuntimeModule(name) {
|
|
@@ -189,6 +192,8 @@ function registerSecureNow(options = {}) {
|
|
|
189
192
|
if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
|
|
190
193
|
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
191
194
|
if (!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = tracesUrl;
|
|
195
|
+
const headerString = appConfig.headersToString(headers);
|
|
196
|
+
if (headerString && !process.env.OTEL_EXPORTER_OTLP_HEADERS) process.env.OTEL_EXPORTER_OTLP_HEADERS = headerString;
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
console.log('[securenow] Next.js App -> service.name=%s', serviceName);
|
|
@@ -376,11 +381,11 @@ function registerSecureNow(options = {}) {
|
|
|
376
381
|
// the remote end (ECONNRESET / "socket hang up"). These transient errors
|
|
377
382
|
// sometimes escape as unhandled exceptions or rejections. We catch them
|
|
378
383
|
// here and log at debug level instead of crashing the host app.
|
|
379
|
-
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
384
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
380
385
|
function _isOtlpTransientError(err) {
|
|
381
386
|
if (!err) return false;
|
|
382
387
|
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
383
|
-
if (typeof err.message === 'string' && /socket hang up|ECONNRESET
|
|
388
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
|
|
384
389
|
return false;
|
|
385
390
|
}
|
|
386
391
|
function _looksLikeOtlpStack(err) {
|
|
@@ -389,21 +394,53 @@ function registerSecureNow(options = {}) {
|
|
|
389
394
|
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
390
395
|
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
391
396
|
}
|
|
397
|
+
function _looksLikeConfiguredOtlpEndpoint(err) {
|
|
398
|
+
const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
|
|
399
|
+
try {
|
|
400
|
+
const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
|
|
401
|
+
return hosts.some((host) => host && text.includes(host));
|
|
402
|
+
} catch (_) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
392
406
|
const _diagDebug = otelLogLevel === 'debug';
|
|
407
|
+
let _lastSuppressedOtlpErrorLogAt = 0;
|
|
408
|
+
function _originalConsole(method) {
|
|
409
|
+
const originals = console.__securenow_original || console.__securenowOriginalConsole;
|
|
410
|
+
return (originals && originals[method]) || console[method] || console.log;
|
|
411
|
+
}
|
|
412
|
+
function _formatOtlpError(err) {
|
|
413
|
+
if (!err) return 'unknown error';
|
|
414
|
+
const parts = [err.message || String(err)];
|
|
415
|
+
if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
|
|
416
|
+
if (err.syscall) parts.push(`syscall=${err.syscall}`);
|
|
417
|
+
if (err.hostname) parts.push(`host=${err.hostname}`);
|
|
418
|
+
return parts.join(' ');
|
|
419
|
+
}
|
|
420
|
+
function _reportSuppressedOtlpError(kind, err, origin) {
|
|
421
|
+
if (otelLogLevel === 'none') return;
|
|
422
|
+
const now = Date.now();
|
|
423
|
+
if (!_diagDebug && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
|
|
424
|
+
_lastSuppressedOtlpErrorLogAt = now;
|
|
425
|
+
const method = _diagDebug ? 'debug' : 'error';
|
|
426
|
+
_originalConsole(method).call(
|
|
427
|
+
console,
|
|
428
|
+
'[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
|
|
429
|
+
kind,
|
|
430
|
+
origin || 'async',
|
|
431
|
+
_formatOtlpError(err)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
393
434
|
process.on('uncaughtException', (err, origin) => {
|
|
394
|
-
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
395
|
-
|
|
396
|
-
console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
|
|
397
|
-
}
|
|
435
|
+
if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
|
|
436
|
+
_reportSuppressedOtlpError('error', err, origin);
|
|
398
437
|
return;
|
|
399
438
|
}
|
|
400
439
|
throw err;
|
|
401
440
|
});
|
|
402
441
|
process.on('unhandledRejection', (reason) => {
|
|
403
|
-
if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
|
|
404
|
-
|
|
405
|
-
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
406
|
-
}
|
|
442
|
+
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
443
|
+
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
407
444
|
return;
|
|
408
445
|
}
|
|
409
446
|
throw reason;
|
|
@@ -625,13 +662,14 @@ function registerSecureNow(options = {}) {
|
|
|
625
662
|
// Key and environment come from .securenow/credentials.json (written by
|
|
626
663
|
// login/init or credentials runtime), so no .env entry is needed.
|
|
627
664
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
628
|
-
if (firewallOptions.apiKey
|
|
665
|
+
if (firewallOptions.apiKey) {
|
|
629
666
|
try {
|
|
630
667
|
requireRuntimeModule('./firewall').init({
|
|
631
668
|
apiKey: firewallOptions.apiKey,
|
|
632
669
|
appKey: firewallOptions.appKey,
|
|
633
670
|
environment: deploymentEnvironment || firewallOptions.environment,
|
|
634
671
|
apiUrl: firewallOptions.apiUrl,
|
|
672
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
635
673
|
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
636
674
|
syncInterval: firewallOptions.syncInterval,
|
|
637
675
|
failMode: firewallOptions.failMode,
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -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
|
|
324
|
+
if (firewallOptions.apiKey) {
|
|
322
325
|
try {
|
|
323
326
|
const { init: fwInit } = await import('./firewall.js');
|
|
324
327
|
fwInit({
|
|
@@ -326,6 +329,7 @@ export default defineNitroPlugin(async (nitroApp) => {
|
|
|
326
329
|
appKey: firewallOptions.appKey,
|
|
327
330
|
environment: deploymentEnvironment || firewallOptions.environment,
|
|
328
331
|
apiUrl: firewallOptions.apiUrl,
|
|
332
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
329
333
|
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
330
334
|
syncInterval: firewallOptions.syncInterval,
|
|
331
335
|
failMode: firewallOptions.failMode,
|
package/otel-defaults.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "7.8.1",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"resolve-ip.js",
|
|
133
133
|
"cidr.js",
|
|
134
134
|
"firewall.js",
|
|
135
|
+
"otel-defaults.js",
|
|
135
136
|
"rate-limits.js",
|
|
136
137
|
"rate-limits.d.ts",
|
|
137
138
|
"firewall-only.js",
|
package/tracing.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* Production should mount/copy tokenless runtime credentials to the same path.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
const { defaultMetricsExporterToNone } = require('./otel-defaults');
|
|
18
|
+
defaultMetricsExporterToNone();
|
|
19
|
+
|
|
17
20
|
const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
|
|
18
21
|
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
19
22
|
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
@@ -580,11 +583,11 @@ if (loggingEnabled) {
|
|
|
580
583
|
// request's error path isn't fully covered by the OTel library. We install
|
|
581
584
|
// targeted process-level handlers to catch them and log at debug level instead
|
|
582
585
|
// of crashing the host app.
|
|
583
|
-
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
586
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
584
587
|
function _isOtlpTransientError(err) {
|
|
585
588
|
if (!err) return false;
|
|
586
589
|
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
587
|
-
if (typeof err.message === 'string' && /socket hang up|ECONNRESET
|
|
590
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
|
|
588
591
|
return false;
|
|
589
592
|
}
|
|
590
593
|
function _looksLikeOtlpStack(err) {
|
|
@@ -593,22 +596,55 @@ function _looksLikeOtlpStack(err) {
|
|
|
593
596
|
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
594
597
|
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
595
598
|
}
|
|
599
|
+
function _looksLikeConfiguredOtlpEndpoint(err) {
|
|
600
|
+
const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
|
|
601
|
+
try {
|
|
602
|
+
const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
|
|
603
|
+
return hosts.some((host) => host && text.includes(host));
|
|
604
|
+
} catch (_) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function _originalConsole(method) {
|
|
609
|
+
const originals = console.__securenow_original || console.__securenowOriginalConsole;
|
|
610
|
+
return (originals && originals[method]) || console[method] || console.log;
|
|
611
|
+
}
|
|
612
|
+
let _lastSuppressedOtlpErrorLogAt = 0;
|
|
613
|
+
function _formatOtlpError(err) {
|
|
614
|
+
if (!err) return 'unknown error';
|
|
615
|
+
const parts = [err.message || String(err)];
|
|
616
|
+
if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
|
|
617
|
+
if (err.syscall) parts.push(`syscall=${err.syscall}`);
|
|
618
|
+
if (err.hostname) parts.push(`host=${err.hostname}`);
|
|
619
|
+
return parts.join(' ');
|
|
620
|
+
}
|
|
621
|
+
function _reportSuppressedOtlpError(kind, err, origin) {
|
|
622
|
+
const level = String(diagLevel || '').toLowerCase();
|
|
623
|
+
if (level === 'none') return;
|
|
624
|
+
const now = Date.now();
|
|
625
|
+
if (level !== 'debug' && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
|
|
626
|
+
_lastSuppressedOtlpErrorLogAt = now;
|
|
627
|
+
const method = level === 'debug' ? 'debug' : 'error';
|
|
628
|
+
_originalConsole(method).call(
|
|
629
|
+
console,
|
|
630
|
+
'[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
|
|
631
|
+
kind,
|
|
632
|
+
origin || 'async',
|
|
633
|
+
_formatOtlpError(err)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
596
636
|
|
|
597
637
|
process.on('uncaughtException', (err, origin) => {
|
|
598
|
-
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
return; // swallow — do not crash
|
|
638
|
+
if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
|
|
639
|
+
_reportSuppressedOtlpError('error', err, origin);
|
|
640
|
+
return; // swallow - do not crash
|
|
603
641
|
}
|
|
604
642
|
// Not ours — re-throw so the default handler (or the app's own handler) fires
|
|
605
643
|
throw err;
|
|
606
644
|
});
|
|
607
645
|
process.on('unhandledRejection', (reason) => {
|
|
608
|
-
if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
|
|
609
|
-
|
|
610
|
-
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
611
|
-
}
|
|
646
|
+
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
647
|
+
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
612
648
|
return; // swallow
|
|
613
649
|
}
|
|
614
650
|
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
@@ -663,12 +699,13 @@ const sdk = new NodeSDK({
|
|
|
663
699
|
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
664
700
|
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
665
701
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
666
|
-
if (firewallOptions.apiKey
|
|
702
|
+
if (firewallOptions.apiKey) {
|
|
667
703
|
require('./firewall').init({
|
|
668
704
|
apiKey: firewallOptions.apiKey,
|
|
669
705
|
appKey: firewallOptions.appKey,
|
|
670
706
|
environment: firewallOptions.environment,
|
|
671
707
|
apiUrl: firewallOptions.apiUrl,
|
|
708
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
672
709
|
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
673
710
|
syncInterval: firewallOptions.syncInterval,
|
|
674
711
|
failMode: firewallOptions.failMode,
|
package/web-vite.mjs
CHANGED
|
@@ -145,6 +145,9 @@ const tracesUrl =
|
|
|
145
145
|
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
146
146
|
const deploymentEnvironment =
|
|
147
147
|
env('SECURENOW_DEPLOYMENT_ENVIRONMENT') || env('SECURENOW_ENVIRONMENT') || viteEnv.MODE || 'production';
|
|
148
|
+
const browserAppKey = env('SECURENOW_APPID');
|
|
149
|
+
if (browserAppKey && !headers['x-api-key']) headers['x-api-key'] = browserAppKey;
|
|
150
|
+
if (deploymentEnvironment && !headers['x-securenow-environment']) headers['x-securenow-environment'] = deploymentEnvironment;
|
|
148
151
|
|
|
149
152
|
// ---- naming rules (mirrors tracing.js) ----
|
|
150
153
|
const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
|