securenow 7.7.16 → 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 +48 -24
- package/SKILL-CLI.md +61 -40
- package/app-config.js +79 -16
- 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 +5 -6
- 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 +3 -4
- package/firewall.js +110 -35
- package/mcp/catalog.js +43 -30
- package/mcp/server.js +73 -12
- package/nextjs.js +4 -1
- package/nuxt-server-plugin.mjs +7 -4
- package/otel-defaults.js +11 -0
- package/package.json +2 -1
- package/tracing.js +4 -1
package/mcp/catalog.js
CHANGED
|
@@ -10,14 +10,15 @@ const MARKDOWN = 'text/markdown';
|
|
|
10
10
|
const UNIVERSAL_SECURENOW_SETUP_PROMPT = `You are working in an existing JavaScript or TypeScript app. Set up SecureNow end-to-end for the framework/runtime already used by this repo. Treat this as a real onboarding, not just a package install.
|
|
11
11
|
|
|
12
12
|
Primary goals:
|
|
13
|
-
- Use the latest published SecureNow npm package. Require securenow@7.
|
|
13
|
+
- Use the latest published SecureNow npm package. Require securenow@7.8.0 or newer for split admin/runtime credentials.
|
|
14
14
|
- By default, enable tracing, logs, POST request body capture, multipart metadata capture, and the SecureNow firewall.
|
|
15
15
|
- If I explicitly ask for firewall-only mode, keep the same install/login/verification gates, but use firewall-only preload and do not add tracing, logging, or OTel instrumentation.
|
|
16
|
-
- The firewall must protect the selected SecureNow app, use SecureNow's own blocklist/allowlist/IPDB data, and respect that app's SecureNow IPDB confidence threshold. Do not add custom IP reputation providers or custom auto-blocking.
|
|
16
|
+
- The firewall must protect the selected SecureNow app, use SecureNow's own blocklist/allowlist/IPDB data, and respect that app's SecureNow IPDB confidence threshold. Do not add custom IP reputation providers or custom auto-blocking.
|
|
17
|
+
- Do not confuse IP Allowlist with Trusted IPs. IP Allowlist is restrictive deny-by-default: when any allowlist entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. Use Trusted IPs for known-safe monitors, office/VPN traffic, or false-positive suppression. Only use allowlist after explicit human approval to lock the app/environment to known IPs.
|
|
17
18
|
|
|
18
19
|
Safety rules:
|
|
19
|
-
- Do not print full API keys, JWTs, tokens, or .securenow/credentials.json. Mask secrets.
|
|
20
|
-
- Do not commit secrets. Ignore only local SecureNow credential files (.securenow/credentials.json and .securenow/credentials.*.json); keep the .securenow/ directory itself trackable for repo-owned docs/templates.
|
|
20
|
+
- Do not print full API keys, JWTs, tokens, or local SecureNow credential files (.securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, or .securenow/credentials.*.json). Mask secrets.
|
|
21
|
+
- Do not commit secrets. Ignore only local SecureNow credential files (.securenow/admin.json, .securenow/runtime.json, .securenow/credentials.json, and .securenow/credentials.*.json); keep the .securenow/ directory itself trackable for repo-owned docs/templates.
|
|
21
22
|
- Do not manually browse to a SecureNow auth URL. Always start auth with npx securenow login so the CLI generates the required callback and state.
|
|
22
23
|
- If the browser says "Missing callback parameter", you opened the wrong URL: rerun npx securenow login from the project root.
|
|
23
24
|
- Do not skip login, app selection, firewall connection, or verification unless I explicitly say to.
|
|
@@ -31,33 +32,35 @@ Runbook:
|
|
|
31
32
|
2. Install or upgrade SecureNow with the detected package manager, using securenow@latest. Verify the actual installed version with:
|
|
32
33
|
node -p "require('./node_modules/securenow/package.json').version"
|
|
33
34
|
npx securenow version
|
|
34
|
-
Stop and fix the install if either is below 7.
|
|
35
|
+
Stop and fix the install if either is below 7.8.0 or npx still resolves an older local package.
|
|
35
36
|
3. Read the installed package surface before editing files: node_modules/securenow/package.json, README/NPM_README, SKILL-API, SKILL-CLI, npx securenow help, and relevant subcommand help for login/init/firewall/doctor/env/test-span/log/mcp.
|
|
36
|
-
4. Mandatory auth gate:
|
|
37
|
-
- Run npx securenow whoami from the project root.
|
|
38
|
-
- If
|
|
39
|
-
-
|
|
40
|
-
-
|
|
37
|
+
4. Mandatory auth/runtime gate:
|
|
38
|
+
- Run npx securenow whoami from the project root.
|
|
39
|
+
- If admin auth is missing, run npx securenow admin login from the project root and wait for the browser flow.
|
|
40
|
+
- If runtime app config is missing, run npx securenow app connect from the project root and wait for the browser flow.
|
|
41
|
+
- After the CLI exits, rerun npx securenow whoami.
|
|
42
|
+
- Do not proceed to app edits or verification until whoami shows the required lane(s). SDK setup needs runtime app config; admin/global MCP operations need admin auth.
|
|
41
43
|
5. Validate project-local credentials without exposing secrets:
|
|
42
|
-
- Confirm .securenow/credentials.json exists.
|
|
43
|
-
- Confirm
|
|
44
|
-
- Confirm
|
|
45
|
-
- Confirm .securenow/
|
|
46
|
-
|
|
44
|
+
- Confirm .securenow/runtime.json exists for SDK runtime setup, or legacy .securenow/credentials.json exists for old installs.
|
|
45
|
+
- Confirm the runtime file has SecureNow's default config/explanations block.
|
|
46
|
+
- Confirm the runtime file has an app key/name/instance and a firewall API key after app selection.
|
|
47
|
+
- Confirm .securenow/admin.json exists only when admin CLI/MCP auth is needed.
|
|
48
|
+
- Confirm .securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
|
|
49
|
+
6. Run npx securenow init. If it fails with ui.header is not a function or another CLI bug, upgrade to securenow@latest, verify >=7.8.0, and retry. Do not silently ignore init failures.
|
|
47
50
|
7. Configure the least invasive framework-specific integration:
|
|
48
51
|
- Next.js: preserve instrumentation.js/ts. Register securenow/nextjs only when NEXT_RUNTIME is nodejs. In ESM files, use createRequire before require("securenow/nextjs"). Include require("securenow/nextjs-auto-capture") for body capture. For Next 15+, add securenow to serverExternalPackages. For older Next.js, use experimental.serverComponentsExternalPackages. Preserve proxy.js/middleware.js.
|
|
49
52
|
- Nuxt/Nitro: use the documented securenow/nuxt module or Nitro server plugin.
|
|
50
53
|
- Express/Fastify/NestJS/Koa/Hapi/Hono/raw Node: preload securenow/register through existing scripts, NODE_OPTIONS, PM2 node_args, Docker CMD, or the process manager already used.
|
|
51
54
|
- Firewall-only: preload securenow/firewall-only or use the documented securenow run --firewall-only command. Do not add OTel/tracing/logging in this mode.
|
|
52
55
|
- Vite/browser-only: use only documented browser integration and state that server firewall protection requires a server runtime.
|
|
53
|
-
8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/credentials.json:
|
|
56
|
+
8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/runtime.json, with legacy .securenow/credentials.json and generated runtime credential files still supported:
|
|
54
57
|
- config.logging.enabled: true
|
|
55
58
|
- config.capture.body: true
|
|
56
59
|
- config.capture.multipart: true
|
|
57
60
|
- config.firewall.enabled: true
|
|
58
61
|
- config.firewall.failMode: "open"
|
|
59
62
|
- config.capture.maxBodySize: 10240
|
|
60
|
-
For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
|
|
63
|
+
For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
|
|
61
64
|
Local credentials should use config.runtime.deploymentEnvironment="local". Production runtime credentials should use "production". The app key stays the same; traces, logs, firewall status, forensics, and CLI/MCP queries are scoped by environment.
|
|
62
65
|
9. Verify firewall and threshold:
|
|
63
66
|
- Run npx securenow firewall apps and npx securenow firewall status.
|
|
@@ -1108,19 +1111,22 @@ const TOOLS = [
|
|
|
1108
1111
|
{
|
|
1109
1112
|
name: 'securenow_blocklist_add',
|
|
1110
1113
|
title: 'Add Blocked IP',
|
|
1111
|
-
description: 'Add an IP/CIDR to the blocklist. Write action; requires confirmation.',
|
|
1114
|
+
description: 'Add an IP/CIDR to the blocklist, optionally scoped to a route/method. Write action; requires confirmation.',
|
|
1112
1115
|
scope: 'blocklist:write',
|
|
1113
1116
|
readOnly: false,
|
|
1114
1117
|
confirm: true,
|
|
1115
1118
|
method: 'POST',
|
|
1116
1119
|
endpoint: '/blocklist',
|
|
1117
|
-
bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment'],
|
|
1120
|
+
bodyFields: ['ip', 'reason', 'expiresAt', 'metadata', 'appKey', 'environment', 'pathPattern', 'pathMatchMode', 'method'],
|
|
1118
1121
|
inputSchema: objectSchema({
|
|
1119
1122
|
ip: string('IPv4 address or CIDR.'),
|
|
1120
1123
|
reason: string('Reason for blocking.'),
|
|
1121
1124
|
expiresAt: string('Optional expiry time as ISO 8601.'),
|
|
1122
1125
|
metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
|
|
1123
1126
|
appKey: string('Optional application key to scope this block. Omit for all apps.'),
|
|
1127
|
+
pathPattern: string('Optional route/path pattern, for example /admin*. Omit to block all routes.'),
|
|
1128
|
+
pathMatchMode: { type: 'string', enum: ['exact', 'prefix', 'regex'], description: 'Path matching mode. Defaults to prefix.' },
|
|
1129
|
+
method: { type: 'string', enum: ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], description: 'HTTP method scope. Defaults to ALL.' },
|
|
1124
1130
|
...environmentInput,
|
|
1125
1131
|
...confirmSchema,
|
|
1126
1132
|
}, ['ip', 'confirm', 'reason']),
|
|
@@ -1169,8 +1175,8 @@ const TOOLS = [
|
|
|
1169
1175
|
},
|
|
1170
1176
|
{
|
|
1171
1177
|
name: 'securenow_allowlist_list',
|
|
1172
|
-
title: 'List Allowlist',
|
|
1173
|
-
description: 'List
|
|
1178
|
+
title: 'List Restrictive Allowlist',
|
|
1179
|
+
description: 'List restrictive allowlist entries. If any active entry exists for an app/environment, only listed IPs can reach it and all other IPs are blocked. This is not the Trusted IP list.',
|
|
1174
1180
|
scope: 'allowlist:read',
|
|
1175
1181
|
readOnly: true,
|
|
1176
1182
|
method: 'GET',
|
|
@@ -1184,24 +1190,26 @@ const TOOLS = [
|
|
|
1184
1190
|
},
|
|
1185
1191
|
{
|
|
1186
1192
|
name: 'securenow_allowlist_add',
|
|
1187
|
-
title: 'Add
|
|
1188
|
-
description: '
|
|
1193
|
+
title: 'Add Restrictive Allowlist IP',
|
|
1194
|
+
description: 'Dangerous production action: add an IP/CIDR to the restrictive allowlist. Once active, allowlist mode blocks every IP except listed entries. Do not use this to mark an IP trusted or suppress false positives; use securenow_trusted_add instead. Requires explicit human approval confirming deny-all behavior.',
|
|
1189
1195
|
scope: 'allowlist:write',
|
|
1190
1196
|
readOnly: false,
|
|
1191
1197
|
confirm: true,
|
|
1192
1198
|
method: 'POST',
|
|
1193
1199
|
endpoint: '/allowlist',
|
|
1194
|
-
|
|
1200
|
+
fixedBody: { initiatedBy: 'mcp' },
|
|
1201
|
+
bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys', 'environment', 'allowlistDenyAllApproved'],
|
|
1195
1202
|
inputSchema: objectSchema({
|
|
1196
1203
|
ip: string('IPv4 address or CIDR.'),
|
|
1197
1204
|
label: string('Human-readable label.'),
|
|
1198
|
-
reason: string('
|
|
1205
|
+
reason: string('Required human-approved reason. Must acknowledge that allowlist blocks all non-listed IPs and why Trusted IPs is not the correct action.'),
|
|
1199
1206
|
expiresAt: string('Optional expiry time as ISO 8601.'),
|
|
1200
1207
|
applicationsAll: boolean('Apply to all applications.'),
|
|
1201
1208
|
applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
|
|
1209
|
+
allowlistDenyAllApproved: boolean('Must be true only after the user explicitly approves deny-by-default allowlist behavior. Do not set this for trusted IPs, monitors, false positives, or investigation cleanup.'),
|
|
1202
1210
|
...environmentInput,
|
|
1203
1211
|
...confirmSchema,
|
|
1204
|
-
}, ['ip', 'confirm', 'reason']),
|
|
1212
|
+
}, ['ip', 'confirm', 'reason', 'allowlistDenyAllApproved']),
|
|
1205
1213
|
},
|
|
1206
1214
|
{
|
|
1207
1215
|
name: 'securenow_allowlist_remove',
|
|
@@ -1221,7 +1229,7 @@ const TOOLS = [
|
|
|
1221
1229
|
{
|
|
1222
1230
|
name: 'securenow_trusted_list',
|
|
1223
1231
|
title: 'List Trusted IPs',
|
|
1224
|
-
description: 'List trusted IPs.',
|
|
1232
|
+
description: 'List trusted IPs. Trusted IPs are for known-safe traffic and do not turn on deny-by-default allowlist mode.',
|
|
1225
1233
|
scope: 'trusted_ips:read',
|
|
1226
1234
|
readOnly: true,
|
|
1227
1235
|
method: 'GET',
|
|
@@ -1235,7 +1243,7 @@ const TOOLS = [
|
|
|
1235
1243
|
{
|
|
1236
1244
|
name: 'securenow_trusted_add',
|
|
1237
1245
|
title: 'Add Trusted IP',
|
|
1238
|
-
description: 'Add a trusted IP/CIDR. Write action; requires confirmation.',
|
|
1246
|
+
description: 'Add a trusted IP/CIDR for known-safe infrastructure, monitors, office/VPN traffic, or scoped false-positive suppression. This does not enable deny-by-default allowlist mode and is the correct tool when an IP should be trusted without blocking other visitors. Write action; requires confirmation.',
|
|
1239
1247
|
scope: 'trusted_ips:write',
|
|
1240
1248
|
readOnly: false,
|
|
1241
1249
|
confirm: true,
|
|
@@ -1417,7 +1425,7 @@ function promptMessages(name, args = {}) {
|
|
|
1417
1425
|
'Verify SecureNow default-on protection for this project.',
|
|
1418
1426
|
args.appKey ? `App key: ${args.appKey}` : null,
|
|
1419
1427
|
'Check npx securenow whoami, npx securenow api-key show, npx securenow firewall apps, and npx securenow firewall status.',
|
|
1420
|
-
'Confirm traces, logs, capture.body, capture.multipart, and firewall.enabled are enabled by .securenow/
|
|
1428
|
+
'Confirm traces, logs, capture.body, capture.multipart, and firewall.enabled are enabled by .securenow/runtime.json defaults unless explicitly set false. Legacy .securenow/credentials.json is still accepted.',
|
|
1421
1429
|
'Do not print full tokens or API keys.',
|
|
1422
1430
|
].filter(Boolean).join('\n'),
|
|
1423
1431
|
},
|
|
@@ -1436,7 +1444,7 @@ function promptMessages(name, args = {}) {
|
|
|
1436
1444
|
args.appKeys ? `Scope to app keys: ${args.appKeys}` : null,
|
|
1437
1445
|
`Environment scope: ${args.environment || 'production'}.`,
|
|
1438
1446
|
'Use IP intelligence first, then related traces/logs, then recommend remediation.',
|
|
1439
|
-
'Only block
|
|
1447
|
+
'Only block or trust the IP after explicit user confirmation. Never use IP Allowlist for false positives or trusted traffic; allowlist is deny-by-default and blocks all non-listed IPs.',
|
|
1440
1448
|
].filter(Boolean).join('\n'),
|
|
1441
1449
|
},
|
|
1442
1450
|
},
|
|
@@ -1461,6 +1469,7 @@ function promptMessages(name, args = {}) {
|
|
|
1461
1469
|
'Return one clear outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
|
|
1462
1470
|
'Use rate limiting only as temporary soft remediation for repeated route-specific abuse such as login brute force, credential stuffing, scraping/API bursts, enumeration, recon/probing, or repeated noisy payloads where risk/impact evidence is below the block threshold.',
|
|
1463
1471
|
'Do not rate-limit instead of blocking when there is confirmed exploit success, token/data exposure, SSRF reachability, file read, RCE, persistence, malware/C2, or riskScore >= 85 with high-confidence malicious evidence. Do not rate-limit benign false positives, trusted monitors, app-server/proxy attribution problems, isolated one-off requests, or broad noisy rules that need alert tuning.',
|
|
1472
|
+
'Do not create or recommend IP Allowlist entries during investigations unless the user explicitly asks to lock the app/environment to a known set of IPs. For safe monitors, offices, VPNs, or false positives, use Trusted IPs or scoped false-positive exclusions instead.',
|
|
1464
1473
|
confirmWrites
|
|
1465
1474
|
? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block, securenow_rate_limit_create_from_text plus securenow_human_action_decision_report_add(outcome=rate_limited), or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
|
|
1466
1475
|
: 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
|
|
@@ -1487,6 +1496,7 @@ function promptMessages(name, args = {}) {
|
|
|
1487
1496
|
'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
|
|
1488
1497
|
'Use Rate Limit only for repeated route-specific abuse where temporary friction is safer than blocking: login brute force/credential stuffing, password reset/account enumeration, scraping/API bursts, path or ID enumeration, recon/probing, or repeated noisy payloads without confirmed exploit success.',
|
|
1489
1498
|
'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
|
|
1499
|
+
'Do not create or recommend IP Allowlist entries while working this queue unless the user explicitly approves deny-by-default allowlist mode for the whole app/environment. Use Trusted IPs for trusted/bypass cases.',
|
|
1490
1500
|
confirmWrites
|
|
1491
1501
|
? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing IP status, use securenow_human_action_decision_report_add.'
|
|
1492
1502
|
: 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
|
|
@@ -1595,6 +1605,9 @@ function assertConfirmed(tool, args = {}) {
|
|
|
1595
1605
|
if (!args.reason || !String(args.reason).trim()) {
|
|
1596
1606
|
throw new Error(`${tool.name} requires a non-empty reason.`);
|
|
1597
1607
|
}
|
|
1608
|
+
if (tool.name === 'securenow_allowlist_add' && args.allowlistDenyAllApproved !== true) {
|
|
1609
|
+
throw new Error('securenow_allowlist_add is deny-by-default: it blocks every non-listed IP for the scoped app/environment. Pass allowlistDenyAllApproved:true only after explicit human approval. Use securenow_trusted_add for trusted IPs or false positives.');
|
|
1610
|
+
}
|
|
1598
1611
|
}
|
|
1599
1612
|
|
|
1600
1613
|
function buildApiRequest(tool, rawArgs = {}) {
|
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) {
|
|
@@ -659,7 +662,7 @@ function registerSecureNow(options = {}) {
|
|
|
659
662
|
// Key and environment come from .securenow/credentials.json (written by
|
|
660
663
|
// login/init or credentials runtime), so no .env entry is needed.
|
|
661
664
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
662
|
-
if (firewallOptions.apiKey
|
|
665
|
+
if (firewallOptions.apiKey) {
|
|
663
666
|
try {
|
|
664
667
|
requireRuntimeModule('./firewall').init({
|
|
665
668
|
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": "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');
|
|
@@ -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,
|