securenow 7.7.16 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +44 -36
- package/README.md +56 -38
- package/SKILL-API.md +51 -27
- package/SKILL-CLI.md +67 -45
- package/app-config.js +90 -160
- package/cli/apiKey.js +21 -12
- package/cli/apps.js +3 -3
- package/cli/auth.js +114 -32
- package/cli/client.js +14 -13
- package/cli/config.js +219 -52
- package/cli/credentials.js +4 -4
- package/cli/diagnostics.js +5 -6
- package/cli/firewall.js +19 -7
- package/cli/human.js +13 -8
- package/cli/init.js +5 -5
- package/cli/run.js +1 -5
- package/cli/security.js +31 -11
- package/cli/utils.js +2 -3
- package/cli.js +68 -35
- package/console-instrumentation.js +1 -1
- package/firewall-only.js +7 -11
- package/firewall.js +110 -35
- package/mcp/catalog.js +582 -45
- package/mcp/server.js +73 -12
- package/nextjs-auto-capture.js +3 -6
- package/nextjs-middleware.js +2 -4
- package/nextjs-wrapper.js +3 -6
- package/nextjs.js +4 -11
- package/nuxt-server-plugin.mjs +7 -4
- package/otel-defaults.js +11 -0
- package/package.json +3 -3
- package/rate-limits.js +0 -2
- package/register-vite.js +5 -12
- package/register.js +5 -13
- package/resolve-ip.js +1 -1
- package/tracing.d.ts +1 -1
- package/tracing.js +6 -3
- package/web-vite.mjs +58 -62
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 runtime API 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-auto-capture.js
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
|
|
19
19
|
const { trace } = require('@opentelemetry/api');
|
|
20
20
|
const appConfig = require('./app-config');
|
|
21
|
-
const env = appConfig.env;
|
|
22
21
|
|
|
23
22
|
// Default sensitive fields to redact
|
|
24
23
|
const DEFAULT_SENSITIVE_FIELDS = [
|
|
@@ -56,8 +55,8 @@ async function safeBodyCapture(request, span) {
|
|
|
56
55
|
|
|
57
56
|
try {
|
|
58
57
|
const contentType = request.headers.get('content-type') || '';
|
|
59
|
-
const maxBodySize =
|
|
60
|
-
const customSensitiveFields = (
|
|
58
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
59
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
61
60
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
62
61
|
|
|
63
62
|
// Only for supported types
|
|
@@ -127,8 +126,7 @@ async function safeBodyCapture(request, span) {
|
|
|
127
126
|
* Check if body capture is enabled
|
|
128
127
|
*/
|
|
129
128
|
function isBodyCaptureEnabled() {
|
|
130
|
-
|
|
131
|
-
return !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
|
|
129
|
+
return appConfig.boolConfig('capture.body', true);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
/**
|
|
@@ -195,4 +193,3 @@ module.exports = {
|
|
|
195
193
|
isBodyCaptureEnabled,
|
|
196
194
|
};
|
|
197
195
|
|
|
198
|
-
|
package/nextjs-middleware.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
|
|
19
19
|
const appConfig = require('./app-config');
|
|
20
|
-
const env = appConfig.env;
|
|
21
20
|
|
|
22
21
|
// Default sensitive fields to redact
|
|
23
22
|
const DEFAULT_SENSITIVE_FIELDS = [
|
|
@@ -101,8 +100,8 @@ async function middleware(request) {
|
|
|
101
100
|
|
|
102
101
|
try {
|
|
103
102
|
const contentType = request.headers.get('content-type') || '';
|
|
104
|
-
const maxBodySize =
|
|
105
|
-
const customSensitiveFields = (
|
|
103
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
104
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
106
105
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
107
106
|
|
|
108
107
|
// Only capture supported types
|
|
@@ -184,4 +183,3 @@ module.exports = {
|
|
|
184
183
|
};
|
|
185
184
|
|
|
186
185
|
|
|
187
|
-
|
package/nextjs-wrapper.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
const { trace } = require('@opentelemetry/api');
|
|
19
19
|
const appConfig = require('./app-config');
|
|
20
|
-
const env = appConfig.env;
|
|
21
20
|
|
|
22
21
|
// Default sensitive fields to redact
|
|
23
22
|
const DEFAULT_SENSITIVE_FIELDS = [
|
|
@@ -51,8 +50,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
51
50
|
* Capture body from Request object (clone to avoid consuming)
|
|
52
51
|
*/
|
|
53
52
|
async function captureRequestBody(request) {
|
|
54
|
-
|
|
55
|
-
const captureBody = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_BODY') ?? ''));
|
|
53
|
+
const captureBody = appConfig.boolConfig('capture.body', true);
|
|
56
54
|
|
|
57
55
|
if (!captureBody) return;
|
|
58
56
|
if (!['POST', 'PUT', 'PATCH'].includes(request.method)) return;
|
|
@@ -62,8 +60,8 @@ async function captureRequestBody(request) {
|
|
|
62
60
|
|
|
63
61
|
try {
|
|
64
62
|
const contentType = request.headers.get('content-type') || '';
|
|
65
|
-
const maxBodySize =
|
|
66
|
-
const customSensitiveFields = (
|
|
63
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
64
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
67
65
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
68
66
|
|
|
69
67
|
// Only for supported types
|
|
@@ -155,4 +153,3 @@ module.exports = {
|
|
|
155
153
|
DEFAULT_SENSITIVE_FIELDS,
|
|
156
154
|
};
|
|
157
155
|
|
|
158
|
-
|
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) {
|
|
@@ -183,16 +186,6 @@ function registerSecureNow(options = {}) {
|
|
|
183
186
|
const otelLogLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
184
187
|
const isDevelopmentRuntime = process.env.NODE_ENV === 'development';
|
|
185
188
|
|
|
186
|
-
// @vercel/otel still reads OTel process variables internally. These are
|
|
187
|
-
// derived from credentials JSON; customers do not set them.
|
|
188
|
-
if (isVercel) {
|
|
189
|
-
if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = serviceName;
|
|
190
|
-
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpointBase;
|
|
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;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
189
|
console.log('[securenow] Next.js App -> service.name=%s', serviceName);
|
|
197
190
|
console.log('[securenow] Environment: %s', deploymentEnvironment);
|
|
198
191
|
|
|
@@ -659,7 +652,7 @@ function registerSecureNow(options = {}) {
|
|
|
659
652
|
// Key and environment come from .securenow/credentials.json (written by
|
|
660
653
|
// login/init or credentials runtime), so no .env entry is needed.
|
|
661
654
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
662
|
-
if (firewallOptions.apiKey
|
|
655
|
+
if (firewallOptions.apiKey) {
|
|
663
656
|
try {
|
|
664
657
|
requireRuntimeModule('./firewall').init({
|
|
665
658
|
apiKey: firewallOptions.apiKey,
|
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({
|
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": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -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",
|
|
@@ -166,8 +167,7 @@
|
|
|
166
167
|
"@opentelemetry/sdk-node": "0.218.0",
|
|
167
168
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
168
169
|
"@opentelemetry/sdk-trace-web": "2.7.1",
|
|
169
|
-
"@opentelemetry/semantic-conventions": "1.41.1"
|
|
170
|
-
"dotenv": "17.2.1"
|
|
170
|
+
"@opentelemetry/semantic-conventions": "1.41.1"
|
|
171
171
|
},
|
|
172
172
|
"optionalDependencies": {
|
|
173
173
|
"@vercel/otel": "2.1.2"
|
package/rate-limits.js
CHANGED
package/register-vite.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
// node_modules/securenow/register-vite.js (CommonJS, for -r)
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
console.log('[securenow] dotenv loaded for Vite preload');
|
|
7
|
-
} catch {
|
|
8
|
-
console.log('[securenow] dotenv not available, continuing without .env');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Trace the Node process (Vite dev/preview server) using your existing Node tracer
|
|
12
|
-
module.exports = require('./web-vite.mjs');
|
|
1
|
+
// node_modules/securenow/register-vite.js (CommonJS, for -r)
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Trace the Node process (Vite dev/preview server) using your existing Node tracer
|
|
5
|
+
module.exports = require('./web-vite.mjs');
|
package/register.js
CHANGED
|
@@ -4,17 +4,9 @@
|
|
|
4
4
|
// For ESM apps ("type": "module"), this file auto-registers the
|
|
5
5
|
// OpenTelemetry ESM loader hook via module.register() (Node >=20.6).
|
|
6
6
|
// On older Node versions it falls back to a warning.
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
// 1.
|
|
10
|
-
// configuration comes from .securenow/credentials.json via app-config.js.
|
|
11
|
-
try {
|
|
12
|
-
require('dotenv').config({ quiet: true });
|
|
13
|
-
} catch (e) {
|
|
14
|
-
// dotenv is optional.
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// 2. Auto-register the ESM loader hook so customers never need --import
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// 1. Auto-register the ESM loader hook so customers never need --import
|
|
18
10
|
(() => {
|
|
19
11
|
try {
|
|
20
12
|
const fs = require('fs');
|
|
@@ -45,5 +37,5 @@ try {
|
|
|
45
37
|
}
|
|
46
38
|
})();
|
|
47
39
|
|
|
48
|
-
//
|
|
49
|
-
require('./tracing');
|
|
40
|
+
// 2. Run the OTel SDK setup
|
|
41
|
+
require('./tracing');
|
package/resolve-ip.js
CHANGED
|
@@ -6,7 +6,7 @@ const appConfig = require('./app-config');
|
|
|
6
6
|
const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
|
|
7
7
|
const PRIVATE_IP_RE = /^(127\.|::1$|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|f[cd][0-9a-f]{2}:)/i;
|
|
8
8
|
|
|
9
|
-
const trustedProxies = appConfig.
|
|
9
|
+
const trustedProxies = appConfig.listConfig('networking.trustedProxies');
|
|
10
10
|
const trustedProxySet = trustedProxies.length ? new Set(trustedProxies.map(normalizeIp).filter(Boolean)) : null;
|
|
11
11
|
|
|
12
12
|
let _hostIps = null;
|
package/tracing.d.ts
CHANGED
|
@@ -165,7 +165,7 @@ export const loggerProvider: LoggerProvider | null;
|
|
|
165
165
|
* Configuration:
|
|
166
166
|
*
|
|
167
167
|
* Local development reads .securenow/credentials.json first. Run
|
|
168
|
-
* `npx securenow login` to write app identity/
|
|
168
|
+
* `npx securenow login` to write app identity/runtime API key and
|
|
169
169
|
* `npx securenow init` to ensure secure defaults and explanations.
|
|
170
170
|
*
|
|
171
171
|
* Production uses the same file shape. Run
|
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');
|
|
@@ -277,8 +280,8 @@ const endpointBase = resolvedEndpoints.endpointBase;
|
|
|
277
280
|
const tracesUrl = resolvedEndpoints.tracesUrl;
|
|
278
281
|
const logsUrl = resolvedEndpoints.logsUrl;
|
|
279
282
|
|
|
280
|
-
// resolveEndpoints() also adds
|
|
281
|
-
// explicit OTLP headers did not provide
|
|
283
|
+
// resolveEndpoints() also adds app routing and runtime auth headers when
|
|
284
|
+
// explicit OTLP headers did not provide them.
|
|
282
285
|
const headers = resolvedEndpoints.headers;
|
|
283
286
|
|
|
284
287
|
// -------- naming rules --------
|
|
@@ -696,7 +699,7 @@ const sdk = new NodeSDK({
|
|
|
696
699
|
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
697
700
|
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
698
701
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
699
|
-
if (firewallOptions.apiKey
|
|
702
|
+
if (firewallOptions.apiKey) {
|
|
700
703
|
require('./firewall').init({
|
|
701
704
|
apiKey: firewallOptions.apiKey,
|
|
702
705
|
appKey: firewallOptions.appKey,
|
package/web-vite.mjs
CHANGED
|
@@ -12,21 +12,21 @@ import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
|
12
12
|
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
|
13
13
|
|
|
14
14
|
// ---- helpers / browser config ----
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
const DEFAULT_BROWSER_CONFIG = Object.freeze({
|
|
16
|
+
config: {
|
|
17
|
+
otel: {
|
|
18
|
+
endpoint: 'https://ingest.securenow.ai',
|
|
19
|
+
tracesEndpoint: null,
|
|
20
|
+
headers: {},
|
|
21
|
+
},
|
|
22
|
+
runtime: {
|
|
23
|
+
deploymentEnvironment: 'production',
|
|
24
|
+
noUuid: null,
|
|
25
|
+
strict: false,
|
|
26
|
+
testSpan: false,
|
|
27
|
+
hideBanner: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
function createResource(attributes) {
|
|
@@ -39,15 +39,6 @@ function createResource(attributes) {
|
|
|
39
39
|
throw new Error('Unsupported @opentelemetry/resources version');
|
|
40
40
|
}
|
|
41
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
42
|
function getPath(obj, path) {
|
|
52
43
|
if (!obj || !path) return undefined;
|
|
53
44
|
let cur = obj;
|
|
@@ -74,28 +65,27 @@ function toConfigString(value) {
|
|
|
74
65
|
return String(value);
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
}
|
|
68
|
+
function browserConfig() {
|
|
69
|
+
const injected = globalThis.window && globalThis.window.__SECURENOW__;
|
|
70
|
+
return injected && typeof injected === 'object' ? injected : {};
|
|
71
|
+
}
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
73
|
+
function configValue(path, fallback) {
|
|
74
|
+
const injected = getPath(browserConfig(), path);
|
|
75
|
+
if (injected !== undefined && injected !== null && injected !== '') return injected;
|
|
76
|
+
const defaultValue = getPath(DEFAULT_BROWSER_CONFIG, path);
|
|
77
|
+
if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '') return defaultValue;
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function boolConfig(path, fallback = false) {
|
|
82
|
+
const value = configValue(path, fallback);
|
|
83
|
+
if (typeof value === 'boolean') return value;
|
|
84
|
+
if (typeof value === 'number') return value !== 0;
|
|
85
|
+
const text = String(value || '').trim().toLowerCase();
|
|
86
|
+
if (['1', 'true', 'yes', 'on', 'enabled'].includes(text)) return true;
|
|
87
|
+
if (['0', 'false', 'no', 'off', 'disabled'].includes(text)) return false;
|
|
88
|
+
return fallback;
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
function parseHeaders(str) {
|
|
@@ -139,29 +129,30 @@ function normalizeSignalEndpoint(value, signalType) {
|
|
|
139
129
|
|
|
140
130
|
// ---- endpoints (same defaults as tracing.js) ----
|
|
141
131
|
const endpointBase =
|
|
142
|
-
normalizeEndpointBase(
|
|
132
|
+
normalizeEndpointBase(configValue('config.otel.endpoint', 'https://ingest.securenow.ai'));
|
|
143
133
|
const tracesUrl =
|
|
144
|
-
normalizeSignalEndpoint(
|
|
145
|
-
const headers = parseHeaders(
|
|
134
|
+
normalizeSignalEndpoint(configValue('config.otel.tracesEndpoint'), 'traces') || `${endpointBase}/v1/traces`;
|
|
135
|
+
const headers = parseHeaders(toConfigString(configValue('config.otel.headers', {})));
|
|
146
136
|
const deploymentEnvironment =
|
|
147
|
-
|
|
148
|
-
const browserAppKey =
|
|
149
|
-
if (browserAppKey && !headers['x-
|
|
137
|
+
String(configValue('config.runtime.deploymentEnvironment', 'production'));
|
|
138
|
+
const browserAppKey = toConfigString(configValue('app.key'));
|
|
139
|
+
if (browserAppKey && !headers['x-securenow-app-key']) headers['x-securenow-app-key'] = browserAppKey;
|
|
150
140
|
if (deploymentEnvironment && !headers['x-securenow-environment']) headers['x-securenow-environment'] = deploymentEnvironment;
|
|
151
141
|
|
|
152
142
|
// ---- naming rules (mirrors tracing.js) ----
|
|
153
|
-
const rawBase = (
|
|
143
|
+
const rawBase = (toConfigString(configValue('app.key')) || toConfigString(configValue('app.name')) || '').trim().replace(/^['"]|['"]$/g, '');
|
|
154
144
|
const baseName = rawBase || null;
|
|
155
145
|
// Default to no suffix whenever a baseName is resolved: the dashboard filters
|
|
156
146
|
// service.name by exact match, and browsers have no PM2 cluster problem
|
|
157
147
|
// (each tab has its own service.instance.id). Explicit config.runtime.noUuid=false
|
|
158
148
|
// still re-enables the suffix if someone really wants it.
|
|
159
|
-
const noUuidEnv =
|
|
149
|
+
const noUuidEnv = configValue('config.runtime.noUuid');
|
|
160
150
|
const noUuid =
|
|
161
151
|
(noUuidEnv !== undefined && noUuidEnv !== '')
|
|
162
152
|
? /^(1|true)$/i.test(String(noUuidEnv).trim())
|
|
163
153
|
: !!baseName;
|
|
164
|
-
const strict =
|
|
154
|
+
const strict = boolConfig('config.runtime.strict', false);
|
|
155
|
+
const hostedSecureNowIngest = /^https:\/\/ingest\.securenow\.ai(?:\/|$)/i.test(tracesUrl);
|
|
165
156
|
|
|
166
157
|
function uuidv4() {
|
|
167
158
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
@@ -195,6 +186,12 @@ if (baseName) {
|
|
|
195
186
|
}
|
|
196
187
|
}
|
|
197
188
|
|
|
189
|
+
if (!disabled && hostedSecureNowIngest) {
|
|
190
|
+
disabled = true;
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.warn('[securenow/web-vite] Direct browser OTLP ingestion to SecureNow is disabled in v8.0 until browser-safe auth is available. Use the server-side SDK runtime credentials path.');
|
|
193
|
+
}
|
|
194
|
+
|
|
198
195
|
const instancePrefix = baseName || 'securenow';
|
|
199
196
|
const serviceInstanceId = `${instancePrefix}-${uuidv4()}`;
|
|
200
197
|
|
|
@@ -203,10 +200,10 @@ try {
|
|
|
203
200
|
// eslint-disable-next-line no-console
|
|
204
201
|
console.log(
|
|
205
202
|
'[securenow] web preload loaded app.key=%s app.name=%s config.runtime.noUuid=%s config.runtime.strict=%s → service.name=%s instance.id=%s',
|
|
206
|
-
JSON.stringify(
|
|
207
|
-
JSON.stringify(
|
|
208
|
-
JSON.stringify(
|
|
209
|
-
JSON.stringify(
|
|
203
|
+
JSON.stringify(configValue('app.key')),
|
|
204
|
+
JSON.stringify(configValue('app.name')),
|
|
205
|
+
JSON.stringify(configValue('config.runtime.noUuid')),
|
|
206
|
+
JSON.stringify(configValue('config.runtime.strict')),
|
|
210
207
|
serviceName,
|
|
211
208
|
serviceInstanceId
|
|
212
209
|
);
|
|
@@ -229,7 +226,7 @@ export function startSecurenowWeb() {
|
|
|
229
226
|
[S.SERVICE_NAME]: serviceName,
|
|
230
227
|
[S.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
231
228
|
[S.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
|
|
232
|
-
[S.SERVICE_VERSION]:
|
|
229
|
+
[S.SERVICE_VERSION]: toConfigString(configValue('app.version')) || undefined,
|
|
233
230
|
}),
|
|
234
231
|
spanProcessors: [new BatchSpanProcessor(exporter)],
|
|
235
232
|
});
|
|
@@ -250,8 +247,7 @@ export function startSecurenowWeb() {
|
|
|
250
247
|
],
|
|
251
248
|
});
|
|
252
249
|
|
|
253
|
-
|
|
254
|
-
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
250
|
+
if (boolConfig('config.runtime.testSpan', false)) {
|
|
255
251
|
import('@opentelemetry/api').then(api => {
|
|
256
252
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
257
253
|
const span = tracer.startSpan('securenow.startup.smoke.web'); span.end();
|
|
@@ -265,7 +261,7 @@ export function startSecurenowWeb() {
|
|
|
265
261
|
// ---- Free trial banner (browser DOM injection) ----
|
|
266
262
|
function injectFreeTrialBanner() {
|
|
267
263
|
const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
|
|
268
|
-
const hideBanner =
|
|
264
|
+
const hideBanner = boolConfig('config.runtime.hideBanner', false);
|
|
269
265
|
if (hideBanner || !FREE_TRIAL_HOSTS.some(host => endpointBase.includes(host))) return;
|
|
270
266
|
if (typeof document === 'undefined') return;
|
|
271
267
|
|