securenow 7.7.14 → 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/NPM_README.md +65 -121
- package/README.md +19 -24
- package/SKILL-API.md +491 -490
- package/SKILL-CLI.md +8 -8
- package/app-config.js +146 -43
- package/cli/apps.js +589 -597
- package/cli/auth.js +1 -3
- package/cli/config.js +37 -9
- package/cli/credentials.js +1 -1
- package/cli/diagnostics.js +40 -10
- package/cli/init.js +1 -0
- package/firewall-only.js +1 -0
- package/firewall.js +62 -10
- package/free-trial-banner.js +2 -2
- package/mcp/catalog.js +2 -2
- package/nextjs.d.ts +67 -63
- package/nextjs.js +93 -52
- package/nuxt-server-plugin.mjs +7 -11
- package/nuxt.d.ts +42 -38
- package/nuxt.mjs +1 -1
- package/package.json +1 -1
- package/tracing.d.ts +2 -1
- package/tracing.js +75 -57
- package/web-vite.mjs +105 -15
package/tracing.js
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Preload with: node --require securenow/register app.js
|
|
5
5
|
*
|
|
6
6
|
* Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
|
|
7
|
-
* auto-registered via module.register()
|
|
7
|
+
* auto-registered via module.register() — no --import flag needed.
|
|
8
8
|
* On Node 18 with "type": "module", add the hook manually:
|
|
9
9
|
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* dashboard does exact match),
|
|
16
|
-
* 0 pre-login (use suffix to
|
|
17
|
-
* distinguish PM2 cluster workers).
|
|
18
|
-
* SECURENOW_INSTANCE=http://host:4318 # OTLP/HTTP base (default https://freetrial.securenow.ai:4318)
|
|
19
|
-
* OTEL_EXPORTER_OTLP_ENDPOINT=... # alternative base
|
|
20
|
-
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
|
|
21
|
-
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
22
|
-
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
23
|
-
* SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
|
|
24
|
-
* OTEL_LOG_LEVEL=error|warn|info|debug|none
|
|
25
|
-
* SECURENOW_TEST_SPAN=1
|
|
26
|
-
*
|
|
27
|
-
* Safety:
|
|
28
|
-
* SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
|
|
11
|
+
* Config:
|
|
12
|
+
* Runtime config is read from .securenow/credentials.json.
|
|
13
|
+
* Run `npx securenow login` and `npx securenow init` to create it.
|
|
14
|
+
* Production should mount/copy tokenless runtime credentials to the same path.
|
|
29
15
|
*/
|
|
30
16
|
|
|
31
17
|
const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
|
|
@@ -40,8 +26,6 @@ const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongo
|
|
|
40
26
|
const { randomUUID } = require('crypto');
|
|
41
27
|
const appConfig = require('./app-config');
|
|
42
28
|
|
|
43
|
-
const env = appConfig.env;
|
|
44
|
-
|
|
45
29
|
function createResource(attributes) {
|
|
46
30
|
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
47
31
|
return otelResources.resourceFromAttributes(attributes);
|
|
@@ -265,7 +249,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
265
249
|
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
266
250
|
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
267
251
|
if (!hasCliHook && !hasModuleRegister) {
|
|
268
|
-
console.warn('[securenow]
|
|
252
|
+
console.warn('[securenow] âš ï¸ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
269
253
|
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
270
254
|
}
|
|
271
255
|
}
|
|
@@ -274,7 +258,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
274
258
|
})();
|
|
275
259
|
|
|
276
260
|
// -------- diagnostics --------
|
|
277
|
-
const diagLevel = ((
|
|
261
|
+
const diagLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
278
262
|
(() => {
|
|
279
263
|
const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
|
|
280
264
|
diagLevel === 'info' ? DiagLogLevel.INFO :
|
|
@@ -285,7 +269,7 @@ const diagLevel = ((process.env.OTEL_LOG_LEVEL != null ? process.env.OTEL_LOG_LE
|
|
|
285
269
|
})();
|
|
286
270
|
|
|
287
271
|
// -------- endpoints & app resolution --------
|
|
288
|
-
// Resolution order for endpoint/appId/apiKey: .securenow/credentials.json ->
|
|
272
|
+
// Resolution order for endpoint/appId/apiKey: .securenow/credentials.json -> package.json#name -> defaults.
|
|
289
273
|
const resolvedApp = appConfig.resolveAll();
|
|
290
274
|
const resolvedEndpoints = appConfig.resolveEndpoints();
|
|
291
275
|
|
|
@@ -301,10 +285,10 @@ const headers = resolvedEndpoints.headers;
|
|
|
301
285
|
const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
302
286
|
const baseName = rawBase || null;
|
|
303
287
|
// Auto-disables the per-worker suffix when we resolved a routing UUID from
|
|
304
|
-
// credentials
|
|
288
|
+
// credentials — the dashboard does exact-match IN on service.name, so any
|
|
305
289
|
// suffix breaks routing. config.runtime.noUuid can override.
|
|
306
290
|
const noUuid = appConfig.resolveNoUuid();
|
|
307
|
-
const strict =
|
|
291
|
+
const strict = appConfig.boolConfig('runtime.strict', false);
|
|
308
292
|
const inPm2Cluster = !!(process.env.NODE_APP_INSTANCE || process.env.pm_id);
|
|
309
293
|
|
|
310
294
|
// Fail fast in cluster if base is missing (no more "free" names)
|
|
@@ -328,7 +312,7 @@ const instancePrefix = baseName || 'securenow';
|
|
|
328
312
|
const serviceInstanceId = `${instancePrefix}-${randomUUID()}`;
|
|
329
313
|
|
|
330
314
|
// Loud line per worker to prove what was used
|
|
331
|
-
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s
|
|
315
|
+
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=%s instance.id=%s',
|
|
332
316
|
process.pid,
|
|
333
317
|
JSON.stringify(baseName),
|
|
334
318
|
JSON.stringify(endpointBase),
|
|
@@ -339,18 +323,18 @@ console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=
|
|
|
339
323
|
|
|
340
324
|
// -------- instrumentations --------
|
|
341
325
|
const disabledMap = {};
|
|
342
|
-
for (const n of (
|
|
326
|
+
for (const n of appConfig.listConfig('otel.disableInstrumentations')) {
|
|
343
327
|
disabledMap[n] = { enabled: false };
|
|
344
328
|
}
|
|
345
329
|
|
|
346
330
|
// -------- Body Capture Configuration --------
|
|
347
331
|
// Opt-out defaults: set config.capture.body=false to disable.
|
|
348
|
-
const captureBody =
|
|
349
|
-
const maxBodySize =
|
|
350
|
-
const customSensitiveFields = (
|
|
332
|
+
const captureBody = appConfig.boolConfig('capture.body', true);
|
|
333
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
334
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
351
335
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
352
336
|
|
|
353
|
-
const captureMultipart =
|
|
337
|
+
const captureMultipart = appConfig.boolConfig('capture.multipart', true);
|
|
354
338
|
|
|
355
339
|
const BODY_CAPTURE_PATCH = Symbol.for('securenow.bodyCapture.emitPatch');
|
|
356
340
|
|
|
@@ -370,7 +354,7 @@ function installRequestBodyObserver(span, request, contentType) {
|
|
|
370
354
|
if (isMultipartBody && !captureMultipart) {
|
|
371
355
|
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
372
356
|
span.setAttribute('http.request.body.type', 'multipart');
|
|
373
|
-
span.setAttribute('http.request.body.note', 'Multipart capture disabled
|
|
357
|
+
span.setAttribute('http.request.body.note', 'Multipart capture disabled by config.capture.multipart=false');
|
|
374
358
|
return;
|
|
375
359
|
}
|
|
376
360
|
|
|
@@ -537,8 +521,8 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
537
521
|
});
|
|
538
522
|
|
|
539
523
|
// -------- Logging Configuration --------
|
|
540
|
-
// Opt-out default: set =
|
|
541
|
-
const loggingEnabled =
|
|
524
|
+
// Opt-out default: set config.logging.enabled=false to disable.
|
|
525
|
+
const loggingEnabled = appConfig.boolConfig('logging.enabled', true);
|
|
542
526
|
|
|
543
527
|
// Create shared resource for both traces and logs
|
|
544
528
|
const sharedResource = createResource({
|
|
@@ -596,11 +580,11 @@ if (loggingEnabled) {
|
|
|
596
580
|
// request's error path isn't fully covered by the OTel library. We install
|
|
597
581
|
// targeted process-level handlers to catch them and log at debug level instead
|
|
598
582
|
// of crashing the host app.
|
|
599
|
-
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
|
|
583
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
600
584
|
function _isOtlpTransientError(err) {
|
|
601
585
|
if (!err) return false;
|
|
602
586
|
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
603
|
-
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;
|
|
604
588
|
return false;
|
|
605
589
|
}
|
|
606
590
|
function _looksLikeOtlpStack(err) {
|
|
@@ -609,25 +593,58 @@ function _looksLikeOtlpStack(err) {
|
|
|
609
593
|
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
610
594
|
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
611
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
|
+
}
|
|
612
633
|
|
|
613
634
|
process.on('uncaughtException', (err, origin) => {
|
|
614
|
-
if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
return; // swallow — do not crash
|
|
635
|
+
if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
|
|
636
|
+
_reportSuppressedOtlpError('error', err, origin);
|
|
637
|
+
return; // swallow - do not crash
|
|
619
638
|
}
|
|
620
|
-
// Not ours
|
|
639
|
+
// Not ours — re-throw so the default handler (or the app's own handler) fires
|
|
621
640
|
throw err;
|
|
622
641
|
});
|
|
623
642
|
process.on('unhandledRejection', (reason) => {
|
|
624
|
-
if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
|
|
625
|
-
|
|
626
|
-
console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
|
|
627
|
-
}
|
|
643
|
+
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
644
|
+
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
628
645
|
return; // swallow
|
|
629
646
|
}
|
|
630
|
-
// Not ours
|
|
647
|
+
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
631
648
|
throw reason;
|
|
632
649
|
});
|
|
633
650
|
|
|
@@ -651,19 +668,19 @@ const sdk = new NodeSDK({
|
|
|
651
668
|
(async () => {
|
|
652
669
|
try {
|
|
653
670
|
await Promise.resolve(sdk.start?.());
|
|
654
|
-
console.log('[securenow] OTel SDK started
|
|
671
|
+
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
655
672
|
if (loggingEnabled) {
|
|
656
|
-
console.log('[securenow]
|
|
673
|
+
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
657
674
|
} else {
|
|
658
675
|
console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
|
|
659
676
|
}
|
|
660
677
|
if (captureBody) {
|
|
661
|
-
console.log('[securenow]
|
|
678
|
+
console.log('[securenow] 📠Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
662
679
|
}
|
|
663
680
|
if (captureMultipart) {
|
|
664
|
-
console.log('[securenow]
|
|
681
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
665
682
|
}
|
|
666
|
-
if (
|
|
683
|
+
if (appConfig.boolConfig('runtime.testSpan', false)) {
|
|
667
684
|
const api = require('@opentelemetry/api');
|
|
668
685
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
669
686
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
@@ -671,13 +688,13 @@ const sdk = new NodeSDK({
|
|
|
671
688
|
|
|
672
689
|
// Free trial banner
|
|
673
690
|
const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
|
|
674
|
-
if (isFreeTrial(endpointBase) &&
|
|
691
|
+
if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
|
|
675
692
|
patchHttpForBanner();
|
|
676
693
|
}
|
|
677
694
|
|
|
678
|
-
// Firewall
|
|
695
|
+
// Firewall — auto-activates only when a real snk_live_ key is resolvable.
|
|
679
696
|
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
680
|
-
// only an app-routing UUID (or nothing at all)
|
|
697
|
+
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
681
698
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
682
699
|
if (firewallOptions.apiKey && firewallOptions.enabled) {
|
|
683
700
|
require('./firewall').init({
|
|
@@ -685,6 +702,7 @@ const sdk = new NodeSDK({
|
|
|
685
702
|
appKey: firewallOptions.appKey,
|
|
686
703
|
environment: firewallOptions.environment,
|
|
687
704
|
apiUrl: firewallOptions.apiUrl,
|
|
705
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
688
706
|
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
689
707
|
syncInterval: firewallOptions.syncInterval,
|
|
690
708
|
failMode: firewallOptions.failMode,
|
package/web-vite.mjs
CHANGED
|
@@ -11,9 +11,24 @@ import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-u
|
|
|
11
11
|
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
12
12
|
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
13
13
|
|
|
14
|
-
// ---- helpers /
|
|
14
|
+
// ---- helpers / browser config ----
|
|
15
15
|
const viteEnv = import.meta.env || {};
|
|
16
16
|
|
|
17
|
+
const ENV_TO_BROWSER_CONFIG_PATH = Object.freeze({
|
|
18
|
+
SECURENOW_APPID: 'app.key',
|
|
19
|
+
SECURENOW_INSTANCE: 'config.otel.endpoint',
|
|
20
|
+
OTEL_SERVICE_NAME: 'app.name',
|
|
21
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: 'config.otel.endpoint',
|
|
22
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'config.otel.tracesEndpoint',
|
|
23
|
+
OTEL_EXPORTER_OTLP_HEADERS: 'config.otel.headers',
|
|
24
|
+
SECURENOW_NO_UUID: 'config.runtime.noUuid',
|
|
25
|
+
SECURENOW_STRICT: 'config.runtime.strict',
|
|
26
|
+
SECURENOW_TEST_SPAN: 'config.runtime.testSpan',
|
|
27
|
+
SECURENOW_HIDE_BANNER: 'config.runtime.hideBanner',
|
|
28
|
+
SECURENOW_ENVIRONMENT: 'config.runtime.deploymentEnvironment',
|
|
29
|
+
SECURENOW_DEPLOYMENT_ENVIRONMENT: 'config.runtime.deploymentEnvironment',
|
|
30
|
+
});
|
|
31
|
+
|
|
17
32
|
function createResource(attributes) {
|
|
18
33
|
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
19
34
|
return otelResources.resourceFromAttributes(attributes);
|
|
@@ -24,17 +39,62 @@ function createResource(attributes) {
|
|
|
24
39
|
throw new Error('Unsupported @opentelemetry/resources version');
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
function legacyViteEnvFallbackEnabled() {
|
|
43
|
+
const raw =
|
|
44
|
+
viteEnv.SECURENOW_ENABLE_LEGACY_ENV ??
|
|
45
|
+
viteEnv.VITE_SECURENOW_ENABLE_LEGACY_ENV ??
|
|
46
|
+
viteEnv.SECURENOW_ALLOW_ENV_CONFIG ??
|
|
47
|
+
viteEnv.VITE_SECURENOW_ALLOW_ENV_CONFIG;
|
|
48
|
+
return /^(1|true|yes)$/i.test(String(raw || '').trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getPath(obj, path) {
|
|
52
|
+
if (!obj || !path) return undefined;
|
|
53
|
+
let cur = obj;
|
|
54
|
+
for (const part of String(path).split('.')) {
|
|
55
|
+
if (cur == null || typeof cur !== 'object' || !(part in cur)) return undefined;
|
|
56
|
+
cur = cur[part];
|
|
57
|
+
}
|
|
58
|
+
return cur;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function headersToString(headers) {
|
|
62
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return undefined;
|
|
63
|
+
return Object.entries(headers)
|
|
64
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
65
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
66
|
+
.join(',');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toConfigString(value) {
|
|
70
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
71
|
+
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
72
|
+
if (Array.isArray(value)) return value.join(',');
|
|
73
|
+
if (typeof value === 'object') return headersToString(value);
|
|
74
|
+
return String(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
27
77
|
function env(k) {
|
|
28
|
-
//
|
|
78
|
+
// Browser config should be injected as window.__SECURENOW__ by the app.
|
|
79
|
+
const w = globalThis.window;
|
|
80
|
+
const injected = w && w.__SECURENOW__;
|
|
81
|
+
const mappedPath = ENV_TO_BROWSER_CONFIG_PATH[String(k).toUpperCase()];
|
|
82
|
+
if (injected) {
|
|
83
|
+
if (mappedPath) {
|
|
84
|
+
const mappedValue = getPath(injected, mappedPath);
|
|
85
|
+
const resolved = toConfigString(mappedValue);
|
|
86
|
+
if (resolved !== undefined) return resolved;
|
|
87
|
+
}
|
|
88
|
+
if (!mappedPath && k in injected) return toConfigString(injected[k]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Vite env fallbacks are legacy and disabled by default.
|
|
92
|
+
if (!legacyViteEnvFallbackEnabled()) return undefined;
|
|
29
93
|
const direct =
|
|
30
94
|
viteEnv[k] ??
|
|
31
95
|
viteEnv[k.toUpperCase()] ??
|
|
32
96
|
viteEnv[k.toLowerCase()];
|
|
33
97
|
if (direct != null) return String(direct);
|
|
34
|
-
|
|
35
|
-
// Optionally support runtime overrides via window.__SECURENOW__
|
|
36
|
-
const w = globalThis.window;
|
|
37
|
-
if (w && w.__SECURENOW__ && k in w.__SECURENOW__) return String(w.__SECURENOW__[k]);
|
|
38
98
|
return undefined;
|
|
39
99
|
}
|
|
40
100
|
|
|
@@ -51,20 +111,50 @@ function parseHeaders(str) {
|
|
|
51
111
|
return out;
|
|
52
112
|
}
|
|
53
113
|
|
|
114
|
+
function normalizeEndpointBase(value) {
|
|
115
|
+
const endpoint = String(value || '').trim().replace(/\/$/, '');
|
|
116
|
+
if (!endpoint) return endpoint;
|
|
117
|
+
if (endpoint === 'https://api.securenow.ai/api/otlp') return 'https://ingest.securenow.ai';
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const parseable = /^[a-z][a-z0-9+.-]*:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`;
|
|
121
|
+
const url = new URL(parseable);
|
|
122
|
+
if (url.port === '4318' && url.hostname.toLowerCase().endsWith('.securenow.ai')) {
|
|
123
|
+
return 'https://ingest.securenow.ai';
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
|
|
127
|
+
return endpoint;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeSignalEndpoint(value, signalType) {
|
|
131
|
+
if (!value) return value;
|
|
132
|
+
const endpoint = String(value).trim().replace(/\/$/, '');
|
|
133
|
+
const signalPath = `/v1/${signalType}`;
|
|
134
|
+
if (endpoint.endsWith(signalPath)) {
|
|
135
|
+
return `${normalizeEndpointBase(endpoint.slice(0, -signalPath.length))}${signalPath}`;
|
|
136
|
+
}
|
|
137
|
+
return endpoint;
|
|
138
|
+
}
|
|
139
|
+
|
|
54
140
|
// ---- endpoints (same defaults as tracing.js) ----
|
|
55
141
|
const endpointBase =
|
|
56
|
-
(env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'https://
|
|
57
|
-
.replace(/\/$/, '');
|
|
142
|
+
normalizeEndpointBase(env('SECURENOW_INSTANCE') || env('OTEL_EXPORTER_OTLP_ENDPOINT') || 'https://ingest.securenow.ai');
|
|
58
143
|
const tracesUrl =
|
|
59
|
-
env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`;
|
|
144
|
+
normalizeSignalEndpoint(env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'), 'traces') || `${endpointBase}/v1/traces`;
|
|
60
145
|
const headers = parseHeaders(env('OTEL_EXPORTER_OTLP_HEADERS'));
|
|
146
|
+
const deploymentEnvironment =
|
|
147
|
+
env('SECURENOW_DEPLOYMENT_ENVIRONMENT') || env('SECURENOW_ENVIRONMENT') || viteEnv.MODE || 'production';
|
|
148
|
+
const browserAppKey = env('SECURENOW_APPID');
|
|
149
|
+
if (browserAppKey && !headers['x-api-key']) headers['x-api-key'] = browserAppKey;
|
|
150
|
+
if (deploymentEnvironment && !headers['x-securenow-environment']) headers['x-securenow-environment'] = deploymentEnvironment;
|
|
61
151
|
|
|
62
152
|
// ---- naming rules (mirrors tracing.js) ----
|
|
63
153
|
const rawBase = (env('OTEL_SERVICE_NAME') || env('SECURENOW_APPID') || '').trim().replace(/^['"]|['"]$/g, '');
|
|
64
154
|
const baseName = rawBase || null;
|
|
65
155
|
// Default to no suffix whenever a baseName is resolved: the dashboard filters
|
|
66
156
|
// service.name by exact match, and browsers have no PM2 cluster problem
|
|
67
|
-
// (each tab has its own service.instance.id). Explicit
|
|
157
|
+
// (each tab has its own service.instance.id). Explicit config.runtime.noUuid=false
|
|
68
158
|
// still re-enables the suffix if someone really wants it.
|
|
69
159
|
const noUuidEnv = env('SECURENOW_NO_UUID');
|
|
70
160
|
const noUuid =
|
|
@@ -95,7 +185,7 @@ if (baseName) {
|
|
|
95
185
|
serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
|
|
96
186
|
} else {
|
|
97
187
|
if (strict) {
|
|
98
|
-
console.error('[securenow/web-vite] FATAL:
|
|
188
|
+
console.error('[securenow/web-vite] FATAL: SecureNow app key missing and config.runtime.strict=true. Tracing disabled.');
|
|
99
189
|
// @ts-expect-error
|
|
100
190
|
window.__SECURENOW_DISABLED__ = true;
|
|
101
191
|
disabled = true;
|
|
@@ -112,7 +202,7 @@ const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
|
|
|
112
202
|
try {
|
|
113
203
|
// eslint-disable-next-line no-console
|
|
114
204
|
console.log(
|
|
115
|
-
'[securenow] web preload loaded
|
|
205
|
+
'[securenow] web preload loaded app.key=%s app.name=%s config.runtime.noUuid=%s config.runtime.strict=%s → service.name=%s instance.id=%s',
|
|
116
206
|
JSON.stringify(env('SECURENOW_APPID')),
|
|
117
207
|
JSON.stringify(env('OTEL_SERVICE_NAME')),
|
|
118
208
|
JSON.stringify(env('SECURENOW_NO_UUID')),
|
|
@@ -138,7 +228,7 @@ export function startSecurenowWeb() {
|
|
|
138
228
|
resource: createResource({
|
|
139
229
|
[S.SERVICE_NAME]: serviceName,
|
|
140
230
|
[S.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
141
|
-
[S.DEPLOYMENT_ENVIRONMENT]:
|
|
231
|
+
[S.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
|
|
142
232
|
[S.SERVICE_VERSION]: viteEnv.VITE_APP_VERSION || undefined,
|
|
143
233
|
}),
|
|
144
234
|
spanProcessors: [new BatchSpanProcessor(exporter)],
|
|
@@ -174,9 +264,9 @@ export function startSecurenowWeb() {
|
|
|
174
264
|
|
|
175
265
|
// ---- Free trial banner (browser DOM injection) ----
|
|
176
266
|
function injectFreeTrialBanner() {
|
|
177
|
-
const
|
|
267
|
+
const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
|
|
178
268
|
const hideBanner = String(env('SECURENOW_HIDE_BANNER')) === '1';
|
|
179
|
-
if (hideBanner || !endpointBase.includes(
|
|
269
|
+
if (hideBanner || !FREE_TRIAL_HOSTS.some(host => endpointBase.includes(host))) return;
|
|
180
270
|
if (typeof document === 'undefined') return;
|
|
181
271
|
|
|
182
272
|
function create() {
|