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.
@@ -14,7 +14,7 @@ function maskSecret(value) {
14
14
  }
15
15
 
16
16
  function buildRuntimeCredentials(options = {}) {
17
- const creds = config.loadCredentials() || {};
17
+ const creds = config.loadRuntimeCredentials() || {};
18
18
  const deploymentEnvironment =
19
19
  options.environment ||
20
20
  options.env ||
@@ -67,10 +67,10 @@ async function runtime(_args, flags) {
67
67
  const warn = flags.stdout ? (msg) => console.error(`! ${msg}`) : ui.warn;
68
68
 
69
69
  if (!creds.app || !creds.app.key) {
70
- warn('No app key found. Run `npx securenow login` first so telemetry routes to the selected app.');
70
+ warn('No app key found. Run `npx securenow app connect` first so telemetry routes to the selected app.');
71
71
  }
72
72
  if (!creds.apiKey) {
73
- warn('Runtime firewall enforcement key is missing. Run `npx securenow login` or `npx securenow api-key set snk_live_...` before generating production runtime credentials.');
73
+ warn('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` before generating production runtime credentials.');
74
74
  }
75
75
 
76
76
  if (flags.stdout) {
@@ -87,7 +87,7 @@ async function runtime(_args, flags) {
87
87
  ui.info('Credential filename lookup is fixed-order and does not depend on NODE_ENV.');
88
88
  ui.info(`Environment: ${envName}`);
89
89
  ui.info(`App: ${creds.app?.name || '(unnamed)'} ${creds.app?.key ? `(${creds.app.key})` : ''}`);
90
- ui.info(`Firewall key: ${creds.apiKey ? maskSecret(creds.apiKey) : '(missing)'}`);
90
+ ui.info(`Runtime API key: ${creds.apiKey ? maskSecret(creds.apiKey) : '(missing)'}`);
91
91
  ui.info('OAuth token/email were not included.');
92
92
  }
93
93
 
@@ -22,8 +22,8 @@ function resolvedConfig(options = {}) {
22
22
  ? (noUuid ? baseName : `${baseName}-<uuid-per-worker>`)
23
23
  : '(auto-generated)';
24
24
  const apiKey = firewall.apiKey || '';
25
- const firewallEnabled = !!apiKey && firewall.enabled;
26
- const firewallLocalEnabled = firewall.enabled;
25
+ const firewallEnabled = !!apiKey;
26
+ const firewallLocalEnabled = true;
27
27
 
28
28
  return {
29
29
  serviceName,
@@ -359,7 +359,6 @@ async function probe({ endpoint, method = 'POST', headers = {}, body = null, tim
359
359
  function firewallStatusLabel(cfg) {
360
360
  if (cfg.firewallEnabled) return ui.c.green('enabled');
361
361
  if (!cfg.apiKey) return ui.c.dim('disabled (missing firewall API key)');
362
- if (!cfg.firewallLocalEnabled) return ui.c.yellow('disabled (config.firewall.enabled=false)');
363
362
  return ui.c.dim('disabled');
364
363
  }
365
364
 
@@ -491,7 +490,7 @@ async function doctor(_args, flags) {
491
490
  warnings.push('OpenTelemetry diagnostic log level is `none`; provider registration/export errors are hidden. Set config.otel.logLevel to `error`/`warn`, or temporarily run with OTEL_LOG_LEVEL=debug.');
492
491
  }
493
492
  if (!cfg.appKey) {
494
- warnings.push('No app key resolved. Run `npx securenow login` or set app.key in .securenow/credentials.json.');
493
+ warnings.push('No app key resolved. Run `npx securenow app connect` or set app.key in .securenow/runtime.json.');
495
494
  }
496
495
  if (cfg.instance === 'https://ingest.securenow.ai') {
497
496
  warnings.push('Using the SecureNow ingest gateway. Dedicated instances are routed server-side after policy checks.');
@@ -501,9 +500,9 @@ async function doctor(_args, flags) {
501
500
  warnings.push('Using the legacy free-trial collector directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
502
501
  }
503
502
  if (!cfg.apiKey && token) {
504
- warnings.push('CLI/MCP is authenticated with your SecureNow session token. Runtime firewall enforcement key is missing. Run `npx securenow login` or `npx securenow api-key set snk_live_...` to refresh .securenow/credentials.json.');
503
+ warnings.push('Admin CLI/MCP auth is connected, but the runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
505
504
  } else if (!cfg.apiKey) {
506
- warnings.push('Runtime firewall enforcement key is missing. Run `npx securenow login` or `npx securenow api-key set snk_live_...` to refresh .securenow/credentials.json.');
505
+ warnings.push('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
507
506
  }
508
507
 
509
508
  const ok = checks.every((c) => c.ok);
package/cli/firewall.js CHANGED
@@ -32,7 +32,9 @@ async function status(args, flags) {
32
32
  console.log(` ${enabledLabel}`);
33
33
  console.log('');
34
34
  ui.keyValue([
35
- ['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
35
+ ['Blocked IPs', `${data.totalIps} total`],
36
+ ['Global blocked IPs', `${data.globalBlockIps ?? data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
37
+ ['Scoped block rules', String(data.scopedBlockRuleCount ?? 0)],
36
38
  ['App scope', appKey || 'all apps'],
37
39
  ['Environment', data.environment || environment],
38
40
  ['Last updated', data.updatedAt || 'unknown'],
@@ -77,6 +79,8 @@ async function testIp(args, flags) {
77
79
  const appKey = await resolveAppKey(flags);
78
80
  const query = { environment };
79
81
  if (appKey) query.appKey = appKey;
82
+ if (flags.path || flags.route) query.path = flags.path || flags.route;
83
+ if (flags.method) query.method = flags.method;
80
84
  const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`, { query });
81
85
 
82
86
  s.stop(`IP ${ip} checked`);
@@ -99,10 +103,16 @@ async function testIp(args, flags) {
99
103
  console.log(` ${ui.c.dim('Allowlist is active — only listed IPs are permitted')}`);
100
104
  } else {
101
105
  console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is in the blocklist`);
102
- if (data.matchedEntry && data.matchedEntry !== ip) {
103
- console.log(` ${ui.c.dim(`Matched by: ${data.matchedEntry}`)}`);
104
- }
105
- }
106
+ if (data.matchedEntry && data.matchedEntry !== ip) {
107
+ console.log(` ${ui.c.dim(`Matched by: ${data.matchedEntry}`)}`);
108
+ }
109
+ if (data.matchedRule) {
110
+ const scope = data.matchedRule.pathPattern
111
+ ? `${data.matchedRule.method || 'ALL'} ${data.matchedRule.pathMatchMode || 'prefix'}:${data.matchedRule.pathPattern}`
112
+ : data.matchedRule.method || 'ALL';
113
+ console.log(` ${ui.c.dim(`Rule scope: ${scope}`)}`);
114
+ }
115
+ }
106
116
  } else {
107
117
  if (data.allowlisted) {
108
118
  console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is on the allowlist`);
@@ -111,7 +121,9 @@ async function testIp(args, flags) {
111
121
  }
112
122
  }
113
123
  console.log(` ${ui.c.dim(`App scope: ${appKey || 'all apps'} · Environment: ${data.environment || environment}`)}`);
114
- console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
124
+ if (data.path) console.log(` ${ui.c.dim(`Request scope: ${data.method || 'GET'} ${data.path}`)}`);
125
+ if (data.scopedMatchRequiresPath) console.log(` ${ui.c.dim('Scoped block rules exist; pass --path to test route-specific matches')}`);
126
+ console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries (${data.scopedBlockRuleCount || 0} scoped)`)}`);
115
127
  if (data.allowlistActive) {
116
128
  console.log(` ${ui.c.dim('Allowlist is active')}`);
117
129
  }
@@ -136,7 +148,7 @@ async function setEnabled(args, flags, enabled) {
136
148
  requireAuth();
137
149
  const appKey = await resolveAppKey(flags);
138
150
  if (!appKey) {
139
- ui.error('No app selected. Pass --app <key> or run `securenow login` / `securenow apps default <key>`.');
151
+ ui.error('No app selected. Pass --app <key> or run `securenow app connect` / `securenow apps default <key>`.');
140
152
  process.exit(1);
141
153
  }
142
154
 
package/cli/human.js CHANGED
@@ -533,18 +533,23 @@ function mcpPromptText(ref, flags = {}) {
533
533
  : '2. For each task, call securenow_notifications_get and securenow_human_action_report for the notificationId and IP.',
534
534
  '3. Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
535
535
  '4. Open/inspect trace evidence with securenow_traces_show and securenow_logs_for_trace when trace IDs are available.',
536
- '5. Decide one outcome: block the IP, mark a scoped false positive, recommend alert-rule tuning, or skip if evidence is ambiguous.',
537
- '6. For block decisions, call securenow_human_action_block with confirm:true, a precise reason, and a decisionReport with summary/evidence/reviewedHistory/traceIds.',
538
- '7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, a precise reason, and a decisionReport.',
539
- '8. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
540
- '9. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing status, call securenow_human_action_decision_report_add or securenow_human_case_decision_report_add with the missing proof.',
541
- '10. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
542
- '11. Summarize each row handled, skipped, rule tuning needed, and still waiting. Do not globally trust an IP by default.',
536
+ '5. Decide one outcome: block the IP, rate limit, mark a scoped false positive, apply/prepare alert-rule tuning, create/prepare a new rule, approve/reject a case action, or mark ambiguous/skipped with exact missing proof.',
537
+ '6. Finished does not always mean enforcement changed. If no write is safe, record or prepare a structured no-write decision report with the exact proof gap, missing permission, or vague guard.',
538
+ '7. For mixed evidence, split the decision by IP/cluster. Block only proven malicious IPs, false-positive only exact benign shapes, rate-limit only route-specific volume abuse, and leave ambiguous members with missing proof.',
539
+ '8. If the right fix is global/system rule tuning and MCP returns access denied, do not create customer-specific false positives. Prepare the exact attack-preserving guard/SQL, dry-run it if possible, and record outcome=rule_tuning with missingProof naming the missing permission.',
540
+ '9. Reject vague AI proposed actions. If "shared benign shape" or similar lacks concrete rule id, path, method, status, user-agent/body/header guard, benign samples, and attack examples that still match, do not execute it.',
541
+ '10. For block decisions, call securenow_human_action_block with confirm:true, a precise reason, and a decisionReport with summary/evidence/reviewedHistory/traceIds.',
542
+ '11. For rate-limit decisions, call securenow_rate_limit_create_from_text with confirm:true, then add securenow_human_action_decision_report_add with outcome=rate_limited.',
543
+ '12. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, a precise reason, and a decisionReport.',
544
+ '13. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
545
+ '14. For skipped/ambiguous/rule-tuning-needed/new-rule-needed rows that should be auditable without changing enforcement status, call securenow_human_action_decision_report_add or securenow_human_case_decision_report_add with the missing proof.',
546
+ '15. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
547
+ '16. Summarize handled count, writes executed, verification result, rule tuning/admin permission blockers, partial resolutions, skipped proof gaps, and still waiting. Do not globally trust an IP by default.',
543
548
  '',
544
549
  'Safety:',
545
550
  '- Do not call write tools without confirm:true and a reason.',
546
551
  '- Do not use broad false-positive scopes if the evidence only supports one alert rule/path/method/status/user-agent/body pattern.',
547
- '- Prefer no action over a weak action.',
552
+ '- Prefer an auditable no-write decision report over a weak substitute action.',
548
553
  ].join('\n');
549
554
  }
550
555
 
package/cli/init.js CHANGED
@@ -81,7 +81,7 @@ async function init(_args, flags) {
81
81
 
82
82
  console.log('');
83
83
  ui.success('Setup complete.');
84
- ui.info('Run `npx securenow login` if this project is not linked to an app yet.');
84
+ ui.info('Run `npx securenow app connect` if this project is not linked to an app yet.');
85
85
  ui.info('Then verify with `npx securenow test-span` and `npx securenow status`.');
86
86
  }
87
87
 
@@ -101,10 +101,10 @@ function initCredentials(flags) {
101
101
  process.exit(1);
102
102
  }
103
103
  config.setApiKey(explicitApiKey, { local: true });
104
- ui.success('Stored firewall API key in .securenow/credentials.json');
104
+ ui.success('Stored firewall API key in .securenow/runtime.json');
105
105
  }
106
106
 
107
- ui.success('Ensured .securenow/credentials.json has secure defaults and explanations');
107
+ ui.success('Ensured .securenow/runtime.json has secure defaults and explanations');
108
108
  }
109
109
 
110
110
  async function initNextJs(dir, project, flags) {
@@ -233,8 +233,8 @@ function printAgentPrompt(kind, filename, major, project) {
233
233
  ui.heading('Codex/Claude prompt');
234
234
  console.log([
235
235
  'Set up SecureNow in this existing Next.js project without using .env files.',
236
- 'Use .securenow/credentials.json for local and production configuration; do not add .env files.',
237
- 'Ignore only .securenow/credentials.json and .securenow/credentials.*.json in git; keep the .securenow/ directory itself trackable for repo-owned files.',
236
+ 'Use .securenow/runtime.json for local SDK runtime configuration and .securenow/credentials.<env>.json for production runtime exports; do not add .env files.',
237
+ 'Ignore only SecureNow secret files (.securenow/admin.json, .securenow/runtime.json, and .securenow/credentials*.json); keep the .securenow/ directory itself trackable for repo-owned files.',
238
238
  commands
239
239
  ? `Use the project scripts for verification when appropriate: ${commands}. Ask before starting long-running dev/start servers, and ask which command to use if these scripts are not the right customer workflow.`
240
240
  : 'Ask the customer which command starts, builds, and tests this app because package.json does not expose an obvious script.',
package/cli/run.js CHANGED
@@ -152,11 +152,7 @@ function run(rawArgs) {
152
152
 
153
153
  const nodeExe = process.execPath;
154
154
 
155
- if (process.env.SECURENOW_DEBUG) {
156
- console.log(`[securenow] ${ui.c.dim('exec:')} ${nodeExe} ${finalArgs.join(' ')}`);
157
- }
158
-
159
- const child = spawn(nodeExe, finalArgs, {
155
+ const child = spawn(nodeExe, finalArgs, {
160
156
  stdio: 'inherit',
161
157
  env: process.env,
162
158
  cwd: process.cwd(),
package/cli/security.js CHANGED
@@ -441,7 +441,14 @@ async function alertHistoryList(args, flags) {
441
441
 
442
442
  // ── Blocklist ──
443
443
 
444
- async function blocklistList(args, flags) {
444
+ function describeBlockTarget(entry) {
445
+ const parts = [entry.ip || entry.cidr || '-'];
446
+ if (entry.method && entry.method !== 'ALL') parts.push(entry.method);
447
+ if (entry.pathPattern) parts.push(`${entry.pathMatchMode || 'prefix'}:${entry.pathPattern}`);
448
+ return parts.join(' ');
449
+ }
450
+
451
+ async function blocklistList(args, flags) {
445
452
  requireAuth();
446
453
  const s = ui.spinner('Fetching blocklist');
447
454
  try {
@@ -465,7 +472,7 @@ async function blocklistList(args, flags) {
465
472
  console.log('');
466
473
  const rows = items.map(b => [
467
474
  ui.c.dim(ui.truncate(b._id, 12)),
468
- ui.c.red(b.ip || b.cidr || '—'),
475
+ ui.c.red(describeBlockTarget(b)),
469
476
  ui.truncate(b.reason || '', 40),
470
477
  b.source || '—',
471
478
  b.status || 'active',
@@ -475,7 +482,7 @@ async function blocklistList(args, flags) {
475
482
  b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
476
483
  b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
477
484
  ]);
478
- ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
485
+ ui.table(['ID', 'Target', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
479
486
  console.log('');
480
487
  } catch (err) {
481
488
  s.fail('Failed to fetch blocklist');
@@ -496,11 +503,16 @@ async function blocklistAdd(args, flags) {
496
503
  if (flags.duration) body.duration = flags.duration;
497
504
  if (flags.app) body.appKey = flags.app;
498
505
  if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
499
-
500
- const s = ui.spinner(`Blocking ${ip}`);
501
- try {
502
- await api.post('/blocklist', body);
503
- s.stop(`${ip} added to blocklist`);
506
+ if (flags.route || flags.path || flags.pattern) body.pathPattern = flags.route || flags.path || flags.pattern;
507
+ if (flags.mode || flags['path-mode']) body.pathMatchMode = flags.mode || flags['path-mode'];
508
+ if (flags.method) body.method = flags.method;
509
+
510
+ const target = describeBlockTarget({ ip, method: body.method, pathPattern: body.pathPattern, pathMatchMode: body.pathMatchMode });
511
+ const s = ui.spinner(`Blocking ${target}`);
512
+ try {
513
+ const data = await api.post('/blocklist', body);
514
+ s.stop(`${target} added to blocklist`);
515
+ if (flags.json) ui.json(data);
504
516
  } catch (err) {
505
517
  s.fail('Failed to add to blocklist');
506
518
  throw err;
@@ -611,9 +623,17 @@ async function allowlistAdd(args, flags) {
611
623
  body.applicationKeys = String(flags.app).split(',').map((x) => x.trim()).filter(Boolean);
612
624
  }
613
625
  if (flags.env || flags.environment) body.environment = flags.env || flags.environment;
614
-
615
- const s = ui.spinner(`Allowing ${ip}`);
616
- try {
626
+
627
+ if (!flags.force && !flags.yes) {
628
+ ui.warn('IP Allowlist is deny-by-default: once active, only listed IPs can reach the scoped app/environment and all others are blocked.');
629
+ ui.info('Use `securenow trusted add` instead if this IP should be trusted without locking out normal visitors.');
630
+ const ok = await ui.confirm('Add this IP to the restrictive allowlist?');
631
+ if (!ok) { ui.info('Cancelled'); return; }
632
+ }
633
+ body.allowlistDenyAllApproved = true;
634
+
635
+ const s = ui.spinner(`Allowing ${ip}`);
636
+ try {
617
637
  await api.post('/allowlist', body);
618
638
  s.stop(`${ip} added to allowlist`);
619
639
  } catch (err) {
package/cli/utils.js CHANGED
@@ -39,9 +39,8 @@ function redact(args, flags) {
39
39
  const extra = flags.fields
40
40
  ? String(flags.fields).split(',').map((s) => s.trim()).filter(Boolean)
41
41
  : [];
42
- const envExtra = (appConfig.env('SECURENOW_SENSITIVE_FIELDS') || '')
43
- .split(',').map((s) => s.trim()).filter(Boolean);
44
- const fields = [...DEFAULT_SENSITIVE_FIELDS, ...envExtra, ...extra];
42
+ const configExtra = appConfig.listConfig('capture.sensitiveFields');
43
+ const fields = [...DEFAULT_SENSITIVE_FIELDS, ...configExtra, ...extra];
45
44
 
46
45
  const result = redactSensitiveData(parsed, fields);
47
46
 
package/cli.js CHANGED
@@ -40,12 +40,12 @@ function parseArgs(argv) {
40
40
 
41
41
  const COMMANDS = {
42
42
  init: {
43
- desc: 'Set up SecureNow in the current project (instrumentation + .securenow credentials)',
43
+ desc: 'Set up SecureNow in the current project (instrumentation + runtime credentials)',
44
44
  usage: 'securenow init [--env local] [--key <API_KEY>] [--ts|--js] [--src|--root] [--force]',
45
45
  flags: {
46
- env: 'Deployment environment to write into .securenow/credentials.json (default: local)',
46
+ env: 'Deployment environment to write into .securenow/runtime.json (default: local)',
47
47
  environment: 'Alias for --env',
48
- key: 'Firewall API key to write to .securenow/credentials.json',
48
+ key: 'Firewall API key to write to .securenow/runtime.json',
49
49
  'api-key': 'Alias for --key',
50
50
  typescript: 'Force TypeScript',
51
51
  javascript: 'Force JavaScript',
@@ -55,42 +55,78 @@ const COMMANDS = {
55
55
  },
56
56
  run: (a, f) => require('./cli/init').init(a, f),
57
57
  },
58
- login: {
59
- desc: 'Authenticate with SecureNow (saves to project .securenow/ by default)',
60
- usage: 'securenow login [--token <TOKEN>] [--global]',
58
+ login: {
59
+ desc: 'Onboard SecureNow admin auth and app runtime config',
60
+ usage: 'securenow login [--token <TOKEN>] [--global]',
61
61
  flags: {
62
62
  token: 'Authenticate with a token directly',
63
63
  global: 'Save credentials to ~/.securenow/ (shared across all projects)',
64
- },
65
- run: (a, f) => require('./cli/auth').login(a, f),
66
- },
67
- logout: {
68
- desc: 'Clear stored credentials',
69
- usage: 'securenow logout [--global]',
70
- flags: { global: 'Clear global credentials (~/.securenow/) instead of project-local' },
71
- run: (a, f) => require('./cli/auth').logout(a, f),
72
- },
73
- whoami: {
74
- desc: 'Show current session info',
64
+ },
65
+ run: (a, f) => require('./cli/auth').login(a, f),
66
+ },
67
+ admin: {
68
+ desc: 'Manage admin/control-plane CLI and MCP auth',
69
+ usage: 'securenow admin <subcommand> [options]',
70
+ sub: {
71
+ login: {
72
+ desc: 'Authenticate admin/control-plane CLI and MCP tools',
73
+ usage: 'securenow admin login [--token <TOKEN>] [--global]',
74
+ flags: {
75
+ token: 'Authenticate with a token directly',
76
+ global: 'Save admin auth to ~/.securenow/admin.json',
77
+ },
78
+ run: (a, f) => require('./cli/auth').adminLogin(a, f),
79
+ },
80
+ logout: {
81
+ desc: 'Clear admin/control-plane auth',
82
+ usage: 'securenow admin logout [--global]',
83
+ flags: { global: 'Clear global admin auth (~/.securenow/admin.json)' },
84
+ run: (a, f) => require('./cli/auth').logout(a, f),
85
+ },
86
+ },
87
+ defaultSub: 'login',
88
+ },
89
+ app: {
90
+ desc: 'Connect the current project runtime to a SecureNow app',
91
+ usage: 'securenow app <subcommand> [options]',
92
+ sub: {
93
+ connect: {
94
+ desc: 'Select/create app, mint runtime API key, and write SDK runtime config',
95
+ usage: 'securenow app connect [--global]',
96
+ flags: { global: 'Save runtime config to ~/.securenow/runtime.json' },
97
+ run: (a, f) => require('./cli/auth').appConnect(a, f),
98
+ },
99
+ },
100
+ defaultSub: 'connect',
101
+ },
102
+ logout: {
103
+ desc: 'Clear admin/control-plane auth',
104
+ usage: 'securenow logout [--global]',
105
+ flags: { global: 'Clear global admin auth (~/.securenow/admin.json)' },
106
+ run: (a, f) => require('./cli/auth').logout(a, f),
107
+ },
108
+ whoami: {
109
+ desc: 'Show admin auth and SDK runtime status',
75
110
  usage: 'securenow whoami',
76
111
  run: () => require('./cli/auth').whoami(),
77
- },
112
+ },
78
113
  'api-key': {
79
- desc: 'Manage the firewall API key stored in .securenow/credentials.json',
114
+ desc: 'Manage the runtime API key stored in runtime credentials',
80
115
  usage: 'securenow api-key <subcommand> [options]',
81
116
  sub: {
82
117
  create: {
83
- desc: 'Create a firewall API key with your session token and save it',
84
- usage: 'securenow api-key create [name] [--name <name>] [--preset firewall] [--global]',
118
+ desc: 'Create a runtime API key with your session token and save it',
119
+ usage: 'securenow api-key create [name] [--name <name>] [--preset runtime_app] [--app <appKey>] [--global]',
85
120
  flags: {
86
121
  name: 'Human-readable key name',
87
- preset: 'API key preset to create (default: firewall)',
122
+ preset: 'API key preset to create (default: runtime_app)',
123
+ app: 'Application key/id to scope this key to',
88
124
  global: 'Save to ~/.securenow/ instead of project-local',
89
125
  },
90
126
  run: (a, f) => require('./cli/apiKey').create(a, f),
91
127
  },
92
128
  set: {
93
- desc: 'Save an API key (snk_live_...) to the credentials file',
129
+ desc: 'Save an API key (snk_live_...) to runtime credentials',
94
130
  usage: 'securenow api-key set <snk_live_...> [--global]',
95
131
  flags: { global: 'Save to ~/.securenow/ instead of project-local' },
96
132
  run: (a, f) => require('./cli/apiKey').set(a, f),
@@ -273,7 +309,7 @@ const COMMANDS = {
273
309
  apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
274
310
  enable: { desc: 'Turn the firewall ON for an app environment', usage: 'securenow firewall enable [--app <key>] [--env production]', flags: { app: 'App key (defaults to logged-in app)', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').enable(a, f) },
275
311
  disable: { desc: 'Turn the firewall OFF for an app environment', usage: 'securenow firewall disable [--app <key>] [--env local]', flags: { app: 'App key (defaults to logged-in app)', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').disable(a, f) },
276
- 'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip> [--env production]', flags: { env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').testIp(a, f) },
312
+ 'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip> [--path /admin/users] [--method GET] [--env production]', flags: { env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/firewall').testIp(a, f) },
277
313
  },
278
314
  defaultSub: 'status',
279
315
  },
@@ -379,10 +415,10 @@ const COMMANDS = {
379
415
  blocklist: {
380
416
  desc: 'Manage IP blocklist',
381
417
  usage: 'securenow blocklist <subcommand> [options]',
382
- flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', status: 'Entry status: active or removed', 'approval-status': 'Approval filter: pending, approved, or rejected', search: 'IP prefix search', view: 'List view: all or operational', reason: 'Audit reason for unblock', json: 'Output as JSON' },
418
+ flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', status: 'Entry status: active or removed', 'approval-status': 'Approval filter: pending, approved, or rejected', search: 'IP prefix search', view: 'List view: all or operational', reason: 'Audit reason for unblock', route: 'Route pattern such as /admin*', path: 'Alias for --route', pattern: 'Alias for --route', mode: 'Route match mode: prefix, exact, or regex', 'path-mode': 'Alias for --mode', method: 'HTTP method, or ALL', duration: 'Expiry, e.g. 24h or 7d', json: 'Output as JSON' },
383
419
  sub: {
384
420
  list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
385
- add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--app <key>] [--env production] [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
421
+ add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--route /admin*] [--mode prefix] [--method GET] [--app <key>] [--env production] [--duration 24h] [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
386
422
  unblock: { desc: 'Unblock an IP and keep block history', usage: 'securenow blocklist unblock <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
387
423
  remove: { desc: 'Unblock an IP (compatibility alias)', usage: 'securenow blocklist remove <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
388
424
  stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
@@ -736,12 +772,9 @@ async function main() {
736
772
  }
737
773
  }
738
774
 
739
- main().catch((err) => {
740
- if (err.name !== 'CLIError') {
741
- ui.error(err.message || 'An unexpected error occurred');
742
- }
743
- if (process.env.SECURENOW_DEBUG) {
744
- console.error(err.stack || err);
745
- }
746
- process.exit(1);
747
- });
775
+ main().catch((err) => {
776
+ if (err.name !== 'CLIError') {
777
+ ui.error(err.message || 'An unexpected error occurred');
778
+ }
779
+ process.exit(1);
780
+ });
@@ -24,7 +24,7 @@
24
24
  const tracing = require('./tracing');
25
25
 
26
26
  if (!tracing.isLoggingEnabled()) {
27
- console.warn('[securenow] Console instrumentation loaded but logging is disabled (SECURENOW_LOGGING_ENABLED=0).');
27
+ console.warn('[securenow] Console instrumentation loaded but logging is disabled (config.logging.enabled=false).');
28
28
  }
29
29
 
30
30
  // Get a logger instance
package/firewall-only.js CHANGED
@@ -7,20 +7,16 @@
7
7
  * node -r securenow/firewall-only app.js
8
8
  * NODE_OPTIONS='-r securenow/firewall-only' next start
9
9
  *
10
- * Reads .securenow/credentials.json first, with legacy env vars supported only
11
- * as fallbacks. Initialises the HTTP-level firewall when a snk_live_ API key
12
- * is resolvable.
13
- */
14
-
15
- try { require('dotenv').config({ quiet: true }); } catch (_) {}
16
-
10
+ * Reads SecureNow runtime credentials from .securenow/*.json. Initialises the
11
+ * HTTP-level firewall when a snk_live_ API key is resolvable.
12
+ */
13
+
17
14
  const appConfig = require('./app-config');
18
15
  const firewallOptions = appConfig.resolveFirewallOptions();
19
16
 
20
- // config.firewall.enabled=false disables the local SDK firewall.
21
- // In all other cases the toggle lives in the dashboard; default ON when an
22
- // API key is present.
23
- if (firewallOptions.apiKey && firewallOptions.enabled) {
17
+ // Runtime enable/disable is controlled by the dashboard/API toggle. If a
18
+ // firewall key is present, start sync so the remote toggle can be applied.
19
+ if (firewallOptions.apiKey) {
24
20
  require('./firewall').init({
25
21
  apiKey: firewallOptions.apiKey,
26
22
  appKey: firewallOptions.appKey,