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/firewall.js CHANGED
@@ -17,10 +17,12 @@ let _initialized = false;
17
17
  let _consecutiveErrors = 0;
18
18
  let _layers = [];
19
19
  let _rawIps = [];
20
+ let _blocklistRules = [];
20
21
  let _stats = { syncs: 0, blocked: 0, rateLimited: 0, allowed: 0, versionChecks: 0, errors: 0, suppressedDisabled: 0 };
21
22
  let _localhostFallbackTried = false;
22
23
  let _eventQueue = [];
23
24
  let _eventTimer = null;
25
+ let _remainingApiUrlFallbacks = [];
24
26
 
25
27
  // Remote toggle - set by /firewall/sync when an appKey is in scope. Default
26
28
  // true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
@@ -36,8 +38,8 @@ let _lastAllowlistModified = null;
36
38
  let _lastAllowlistVersion = null;
37
39
  let _lastAllowlistSyncEtag = null;
38
40
 
39
- // Rate-limit policy state is synced in Phase 1 for forward compatibility.
40
- // Enforcement is intentionally added in the next SDK phase.
41
+ // Route/method-scoped policy state. Global IP/CIDR blocks stay in _matcher so
42
+ // TCP, OS firewall, and Cloud WAF layers never over-enforce an HTTP-only rule.
41
43
  let _rateLimitRules = [];
42
44
  let _lastRateLimitVersion = null;
43
45
  let _rateLimitBuckets = new Map();
@@ -60,7 +62,7 @@ const _httpsAgent = new https.Agent({ keepAlive: false });
60
62
  const EVENT_FLUSH_INTERVAL_MS = 2_000;
61
63
  const EVENT_BATCH_SIZE = 25;
62
64
  const EVENT_QUEUE_MAX = 1_000;
63
- const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
65
+ const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND']);
64
66
 
65
67
  // Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
66
68
  let _useUnifiedSync = true;
@@ -156,7 +158,13 @@ function agentFor(url) {
156
158
  function isTransientNetworkError(err) {
157
159
  if (!err) return false;
158
160
  if (err.code && TRANSIENT_NETWORK_CODES.has(err.code)) return true;
159
- return /socket hang up|connection reset|ECONNRESET/i.test(String(err.message || ''));
161
+ return /socket hang up|connection reset|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(String(err.message || ''));
162
+ }
163
+
164
+ function isApiReachabilityError(err) {
165
+ if (isTransientNetworkError(err)) return true;
166
+ const text = `${err && err.code || ''} ${err && err.message || ''}`;
167
+ return /TLS|SSL|certificate|CERT_|UNABLE_TO_VERIFY|self signed/i.test(text);
160
168
  }
161
169
 
162
170
  function formatRequestError(err) {
@@ -167,6 +175,33 @@ function formatRequestError(err) {
167
175
  return parts.join(' ');
168
176
  }
169
177
 
178
+ function resetApiUrlFallbacks() {
179
+ const seen = new Set([_options && _options.apiUrl].filter(Boolean));
180
+ _remainingApiUrlFallbacks = [];
181
+ for (const candidate of Array.isArray(_options && _options.apiUrlFallbacks) ? _options.apiUrlFallbacks : []) {
182
+ const url = String(candidate || '').trim().replace(/\/$/, '');
183
+ if (!url || seen.has(url)) continue;
184
+ seen.add(url);
185
+ _remainingApiUrlFallbacks.push(url);
186
+ }
187
+ }
188
+
189
+ function switchToNextApiUrl(reason) {
190
+ if (!_options || _remainingApiUrlFallbacks.length === 0) return false;
191
+ const previous = _options.apiUrl;
192
+ _options.apiUrl = _remainingApiUrlFallbacks.shift();
193
+ _lastUnifiedEtag = null;
194
+ _lastSyncEtag = null;
195
+ _lastAllowlistSyncEtag = null;
196
+ if (_options.log) {
197
+ fwWarn('[securenow] Firewall: %s unreachable (%s), retrying sync via %s',
198
+ previous,
199
+ reason || 'network error',
200
+ _options.apiUrl);
201
+ }
202
+ return true;
203
+ }
204
+
170
205
  function requestOnce(method, url, body, extraHeaders, timeout, callback) {
171
206
  const mod = url.startsWith('https') ? https : http;
172
207
  const parsed = new URL(url);
@@ -288,9 +323,44 @@ function reportFirewallEvent(event) {
288
323
  if (_eventQueue.length >= EVENT_BATCH_SIZE) flushFirewallEvents();
289
324
  else scheduleEventFlush();
290
325
  }
291
-
326
+
327
+ function normalizePrefixPattern(pattern) {
328
+ const value = String(pattern || '');
329
+ return value.endsWith('*') ? value.slice(0, -1) : value;
330
+ }
331
+
332
+ function normalizeBlocklistRules(rules) {
333
+ if (!Array.isArray(rules)) return [];
334
+ return rules
335
+ .map((rule) => {
336
+ if (!rule || !rule.ip) return null;
337
+ const pathMatchMode = ['exact', 'prefix', 'regex'].includes(String(rule.pathMatchMode || '').toLowerCase())
338
+ ? String(rule.pathMatchMode).toLowerCase()
339
+ : 'prefix';
340
+ return {
341
+ ...rule,
342
+ ip: String(rule.ip || '').trim(),
343
+ method: String(rule.method || 'ALL').toUpperCase(),
344
+ pathPattern: pathMatchMode === 'prefix'
345
+ ? normalizePrefixPattern(rule.pathPattern)
346
+ : String(rule.pathPattern || '').trim(),
347
+ pathMatchMode,
348
+ };
349
+ })
350
+ .filter(Boolean);
351
+ }
352
+
353
+ function setBlocklistData(ips, rules) {
354
+ const normalizedIps = Array.isArray(ips) ? ips : [];
355
+ _rawIps = normalizedIps;
356
+ _blocklistRules = normalizeBlocklistRules(rules);
357
+ _matcher = createMatcher(normalizedIps);
358
+ _stats.syncs++;
359
+ notifyLayers(normalizedIps);
360
+ }
361
+
292
362
  // Unified Sync (v2 - single request for everything)
293
-
363
+
294
364
  function doUnifiedSync(callback) {
295
365
  const query = new URLSearchParams();
296
366
  if (_options.appKey) query.set('app', _options.appKey);
@@ -355,13 +425,10 @@ function doUnifiedSync(callback) {
355
425
  }
356
426
  }
357
427
 
358
- if (body.blocklistIps) {
359
- _rawIps = body.blocklistIps;
360
- _matcher = createMatcher(body.blocklistIps);
361
- _stats.syncs++;
362
- notifyLayers(body.blocklistIps);
363
- blChanged = true;
364
- }
428
+ if (body.blocklistIps || body.blocklistRules) {
429
+ setBlocklistData(body.blocklistIps || [], body.blocklistRules || []);
430
+ blChanged = true;
431
+ }
365
432
 
366
433
  // Update allowlist version + data
367
434
  if (body.allowlist) {
@@ -411,16 +478,13 @@ function legacyBlocklistSync(callback) {
411
478
  if (res.statusCode === 429) { handleRetryAfter(res); }
412
479
  if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
413
480
 
414
- try {
415
- const body = JSON.parse(data);
416
- const ips = body.ips || [];
417
- _rawIps = ips;
418
- _matcher = createMatcher(ips);
419
- _lastModified = res.headers['last-modified'] || null;
420
- if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
421
- _stats.syncs++;
422
- notifyLayers(ips);
423
- callback(null, true, _matcher.stats());
481
+ try {
482
+ const body = JSON.parse(data);
483
+ const ips = body.ips || [];
484
+ setBlocklistData(ips, body.rules || body.blocklistRules || []);
485
+ _lastModified = res.headers['last-modified'] || null;
486
+ if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
487
+ callback(null, true, _matcher.stats());
424
488
  } catch (e) {
425
489
  callback(new Error(`Failed to parse blocklist: ${e.message}`));
426
490
  }
@@ -562,9 +626,15 @@ function pollOnce(callback) {
562
626
 
563
627
  const done = (err, result) => {
564
628
  _pollInflight = false;
565
- if (err) {
566
- _consecutiveErrors++;
567
- _stats.errors++;
629
+ if (err) {
630
+ if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
631
+ _pollInflight = false;
632
+ const retryTimer = setTimeout(() => pollOnce(callback), 1000);
633
+ if (retryTimer.unref) retryTimer.unref();
634
+ return;
635
+ }
636
+ _consecutiveErrors++;
637
+ _stats.errors++;
568
638
  maybeOpenCircuit();
569
639
  if (_options.log) fwWarn('[securenow] Firewall: poll failed:', formatRequestError(err));
570
640
  return callback(err);
@@ -572,16 +642,17 @@ function pollOnce(callback) {
572
642
  _consecutiveErrors = 0;
573
643
  resetCircuit();
574
644
  if (result) {
575
- if (result.blChanged && _options.log && _matcher) {
576
- const s = _matcher.stats();
577
- fwLog('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
578
- }
645
+ if (result.blChanged && _options.log && _matcher) {
646
+ const s = _matcher.stats();
647
+ fwLog('[securenow] Firewall: re-synced %d global blocked IPs (%d exact + %d CIDR ranges) and %d scoped block rules',
648
+ s.total, s.exact, s.cidr, _blocklistRules.length);
649
+ }
579
650
  if (result.alChanged && _options.log && _allowlistMatcher) {
580
651
  const s = _allowlistMatcher.stats();
581
652
  fwLog('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
582
653
  }
583
654
  if (result.rlChanged && _options.log) {
584
- fwLog('[securenow] Firewall: re-synced %d rate-limit rules (enforcement pending SDK phase 2)', _rateLimitRules.length);
655
+ fwLog('[securenow] Firewall: re-synced %d rate-limit rules', _rateLimitRules.length);
585
656
  }
586
657
  }
587
658
  callback(null);
@@ -623,9 +694,14 @@ function startSyncLoop() {
623
694
  const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
624
695
 
625
696
  syncFn((err, result) => {
626
- if (err) {
627
- const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
628
- if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
697
+ if (err) {
698
+ const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
699
+ if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
700
+ const retryTimer = setTimeout(initialSync, 1000);
701
+ if (retryTimer.unref) retryTimer.unref();
702
+ return;
703
+ }
704
+ if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
629
705
  _localhostFallbackTried = true;
630
706
  const origUrl = _options.apiUrl;
631
707
  _options.apiUrl = 'http://localhost:4000';
@@ -656,17 +732,18 @@ function startSyncLoop() {
656
732
  return;
657
733
  }
658
734
 
659
- _initialized = true;
660
- if (_options.log && _matcher) {
661
- const s = _matcher.stats();
662
- fwLog('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
663
- }
735
+ _initialized = true;
736
+ if (_options.log && _matcher) {
737
+ const s = _matcher.stats();
738
+ fwLog('[securenow] Firewall: synced %d global blocked IPs (%d exact + %d CIDR ranges) and %d scoped block rules',
739
+ s.total, s.exact, s.cidr, _blocklistRules.length);
740
+ }
664
741
  if (_options.log && _allowlistMatcher) {
665
742
  const s = _allowlistMatcher.stats();
666
743
  if (s.total > 0) fwLog('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
667
744
  }
668
745
  if (_options.log && _rateLimitRules.length > 0) {
669
- fwLog('[securenow] Firewall: synced %d rate-limit rules (enforcement pending SDK phase 2)', _rateLimitRules.length);
746
+ fwLog('[securenow] Firewall: synced %d rate-limit rules', _rateLimitRules.length);
670
747
  }
671
748
  });
672
749
  }
@@ -814,7 +891,7 @@ function rulePathMatches(rule, path) {
814
891
  if (mode === 'regex') {
815
892
  try { return new RegExp(pattern).test(path); } catch (_) { return false; }
816
893
  }
817
- return path.startsWith(pattern);
894
+ return path.startsWith(normalizePrefixPattern(pattern));
818
895
  }
819
896
 
820
897
  function rateLimitKey(rule, ip) {
@@ -823,6 +900,28 @@ function rateLimitKey(rule, ip) {
823
900
  return `${id}|${subject}`;
824
901
  }
825
902
 
903
+ function checkBlocklistRules(req, ip) {
904
+ if (!_blocklistRules || _blocklistRules.length === 0) return null;
905
+ const method = String(req.method || 'GET').toUpperCase();
906
+ const path = requestPath(req);
907
+
908
+ for (const rule of _blocklistRules) {
909
+ if (!rule) continue;
910
+ const ruleMethod = String(rule.method || 'ALL').toUpperCase();
911
+ if (ruleMethod !== 'ALL' && ruleMethod !== method) continue;
912
+ if (!ruleIpMatches(rule, ip)) continue;
913
+ if (!rulePathMatches(rule, path)) continue;
914
+ return {
915
+ rule,
916
+ ruleId: rule.id || rule._id || '',
917
+ matchedEntry: rule.ip || ip,
918
+ path,
919
+ };
920
+ }
921
+
922
+ return null;
923
+ }
924
+
826
925
  function checkRateLimitRules(req, ip) {
827
926
  if (!_rateLimitRules || _rateLimitRules.length === 0) return null;
828
927
  const method = String(req.method || 'GET').toUpperCase();
@@ -918,6 +1017,24 @@ function firewallRequestHandler(req, res) {
918
1017
  return true;
919
1018
  }
920
1019
 
1020
+ const blockRuleDecision = checkBlocklistRules(req, ip);
1021
+ if (blockRuleDecision) {
1022
+ _stats.blocked++;
1023
+ if (_options && _options.log) {
1024
+ fwLog('[securenow] Firewall: blocked %s via HTTP (rule=%s)', ip, blockRuleDecision.ruleId || 'scoped');
1025
+ }
1026
+ reportFirewallEvent({
1027
+ source: 'blocklist',
1028
+ ip,
1029
+ matchedEntry: blockRuleDecision.matchedEntry || ip,
1030
+ method: req.method || '',
1031
+ path: blockRuleDecision.path || req.url || '',
1032
+ userAgent: req.headers['user-agent'] || '',
1033
+ });
1034
+ sendBlockResponse(req, res, ip);
1035
+ return true;
1036
+ }
1037
+
921
1038
  const rateLimitDecision = checkRateLimitRules(req, ip);
922
1039
  if (rateLimitDecision) {
923
1040
  _stats.rateLimited++;
@@ -981,10 +1098,15 @@ function patchHttpLayer() {
981
1098
 
982
1099
  // Init
983
1100
 
984
- function init(options) {
985
- _options = options;
1101
+ function init(options) {
1102
+ _options = options || {};
1103
+ _options.apiUrl = String(_options.apiUrl || '').trim().replace(/\/$/, '');
1104
+ _localhostFallbackTried = false;
1105
+ _useUnifiedSync = true;
1106
+ resetApiUrlFallbacks();
986
1107
 
987
1108
  if (_options.log) fwLog('[securenow] Firewall: ENABLED');
1109
+ if (_options.log && _options.apiUrl) fwLog('[securenow] Firewall: sync endpoint=%s/api/v1/firewall/sync', _options.apiUrl);
988
1110
  if (_options.log && _options.environment) fwLog('[securenow] Firewall: environment=%s', _options.environment);
989
1111
 
990
1112
  patchHttpLayer();
@@ -1043,8 +1165,11 @@ function shutdown() {
1043
1165
  _consecutiveErrors = 0;
1044
1166
  _pollInflight = false;
1045
1167
  _retryAfterUntil = 0;
1168
+ _localhostFallbackTried = false;
1169
+ _blocklistRules = [];
1046
1170
  _rateLimitRules = [];
1047
1171
  _rateLimitBuckets = new Map();
1172
+ _remainingApiUrlFallbacks = [];
1048
1173
 
1049
1174
  _httpAgent.destroy();
1050
1175
  _httpsAgent.destroy();
@@ -1067,8 +1192,9 @@ function shutdown() {
1067
1192
 
1068
1193
  function getStats() {
1069
1194
  return {
1070
- ..._stats,
1195
+ ..._stats,
1071
1196
  matcher: _matcher ? _matcher.stats() : null,
1197
+ blocklistRules: _blocklistRules.length,
1072
1198
  allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
1073
1199
  rateLimitRules: _rateLimitRules.length,
1074
1200
  rateLimitBuckets: _rateLimitBuckets.size,
@@ -1087,7 +1213,8 @@ function getStats() {
1087
1213
  // as "no IPs to block" without us mutating the cached matcher.
1088
1214
  function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
1089
1215
  function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
1216
+ function getBlocklistRules() { return _remoteEnabled === false ? [] : _blocklistRules.slice(); }
1090
1217
  function getRateLimitRules() { return _remoteEnabled === false ? [] : _rateLimitRules.slice(); }
1091
1218
  function isRemoteEnabled() { return _remoteEnabled !== false; }
1092
1219
 
1093
- module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getRateLimitRules, isRemoteEnabled };
1220
+ module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getBlocklistRules, getRateLimitRules, isRemoteEnabled };
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.5.1 or newer.
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.5.1 or npx still resolves an older local package.
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 not logged in, run npx securenow login from the project root and wait for the browser flow.
39
- - After the CLI exits, rerun npx securenow whoami.
40
- - Do not proceed to app edits or verification until whoami succeeds.
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 it has SecureNow's default config/explanations block.
44
- - Confirm it has an app key/name/instance and a firewall API key after login/app selection.
45
- - Confirm .securenow/credentials.json and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
46
- 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.5.1, and retry. Do not silently ignore init failures.
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 allowed IPs.',
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 Allowed IP',
1188
- description: 'Add an IP/CIDR to the allowlist. Write action; requires confirmation.',
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
- bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys', 'environment'],
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('Reason for allowing.'),
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/credentials.json defaults unless explicitly set false.',
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, allow, or trust the IP after explicit user confirmation.',
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 = {}) {