securenow 7.8.1 → 8.0.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/app-config.js CHANGED
@@ -9,8 +9,8 @@
9
9
  * ./.securenow/credentials.production.json are also accepted when the
10
10
  * canonical file is not present. Filename selection is deterministic and does
11
11
  * not read environment variables.
12
- * Legacy environment fallbacks are disabled by default; every SDK setting has
13
- * a file-backed equivalent so customers do not need .env files.
12
+ * Every SDK setting has a file-backed equivalent so customers do not need
13
+ * .env files.
14
14
  */
15
15
 
16
16
  const fs = require('fs');
@@ -21,7 +21,6 @@ const FREE_TRIAL_INSTANCE = 'https://ingest.securenow.ai';
21
21
  const DEFAULT_API_URL = 'https://api.securenow.ai';
22
22
  const DEFAULT_FIREWALL_API_URL = FREE_TRIAL_INSTANCE;
23
23
  const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
24
- const LEGACY_ENV_FALLBACK_FLAG = 'SECURENOW_ENABLE_LEGACY_ENV';
25
24
  const CONFIG_SCHEMA_VERSION = 2;
26
25
  const CREDENTIAL_FILE_ENVIRONMENTS = Object.freeze([
27
26
  'staging',
@@ -94,7 +93,7 @@ const DEFAULT_CONFIG = Object.freeze({
94
93
 
95
94
  const CONFIG_EXPLANATIONS = Object.freeze({
96
95
  'token': 'Legacy CLI session token. New admin/control-plane auth is written to admin.json. Secret: do not commit.',
97
- 'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by `securenow app connect` or `securenow api-key set`. Secret: do not commit.',
96
+ 'apiKey': 'Scoped runtime API key (`snk_live_...`) minted by `securenow app connect` or `securenow api-key set`. It authenticates telemetry ingestion and firewall sync. Secret: do not commit.',
98
97
  'app.key': 'SecureNow application routing UUID. The SDK uses this as OTel service.name so dashboard queries match exactly.',
99
98
  'app.name': 'Human-readable app label shown in CLI output.',
100
99
  'app.routing': 'Telemetry uses the default SecureNow ingestion gateway and routes by app.key; runtime credentials do not expose per-instance collector URLs.',
@@ -106,7 +105,7 @@ const CONFIG_EXPLANATIONS = Object.freeze({
106
105
  'config.otel.endpoint': 'Optional OTLP base endpoint override for advanced/self-hosted collectors. Leave null for the SecureNow ingestion gateway.',
107
106
  'config.otel.tracesEndpoint': 'Optional full traces endpoint override, for split collectors.',
108
107
  'config.otel.logsEndpoint': 'Optional full logs endpoint override, for split collectors.',
109
- 'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-api-key from app.key when missing.',
108
+ 'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-securenow-app-key from app.key and Authorization from apiKey when missing.',
110
109
  'config.otel.logLevel': 'OpenTelemetry diagnostic log level: error, warn, info, debug, or none.',
111
110
  'config.otel.disableInstrumentations': 'Optional OTel instrumentation package names to disable.',
112
111
  'config.runtime.deploymentEnvironment': 'deployment.environment resource attribute. Set this in the credentials file for production.',
@@ -131,45 +130,6 @@ const CONFIG_EXPLANATIONS = Object.freeze({
131
130
  'config.networking.trustedProxies': 'Additional proxy IPs whose X-Forwarded-For headers should be trusted.',
132
131
  });
133
132
 
134
- const ENV_TO_CONFIG_PATH = Object.freeze({
135
- SECURENOW_LOGGING_ENABLED: 'logging.enabled',
136
- SECURENOW_CAPTURE_BODY: 'capture.body',
137
- SECURENOW_CAPTURE_MULTIPART: 'capture.multipart',
138
- SECURENOW_MAX_BODY_SIZE: 'capture.maxBodySize',
139
- SECURENOW_SENSITIVE_FIELDS: 'capture.sensitiveFields',
140
- SECURENOW_DISABLE_INSTRUMENTATIONS: 'otel.disableInstrumentations',
141
- OTEL_EXPORTER_OTLP_ENDPOINT: 'otel.endpoint',
142
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'otel.tracesEndpoint',
143
- OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: 'otel.logsEndpoint',
144
- OTEL_EXPORTER_OTLP_HEADERS: 'otel.headers',
145
- OTEL_LOG_LEVEL: 'otel.logLevel',
146
- SECURENOW_ENVIRONMENT: 'runtime.deploymentEnvironment',
147
- SECURENOW_DEPLOYMENT_ENVIRONMENT: 'runtime.deploymentEnvironment',
148
- NODE_ENV: 'runtime.deploymentEnvironment',
149
- SECURENOW_NO_UUID: 'runtime.noUuid',
150
- SECURENOW_STRICT: 'runtime.strict',
151
- SECURENOW_TEST_SPAN: 'runtime.testSpan',
152
- SECURENOW_HIDE_BANNER: 'runtime.hideBanner',
153
- SECURENOW_API_URL: 'firewall.apiUrl',
154
- SECURENOW_FIREWALL_VERSION_INTERVAL: 'firewall.versionCheckInterval',
155
- SECURENOW_FIREWALL_SYNC_INTERVAL: 'firewall.syncInterval',
156
- SECURENOW_FIREWALL_FAIL_MODE: 'firewall.failMode',
157
- SECURENOW_FIREWALL_STATUS_CODE: 'firewall.statusCode',
158
- SECURENOW_FIREWALL_LOG: 'firewall.log',
159
- SECURENOW_FIREWALL_TCP: 'firewall.tcp',
160
- SECURENOW_FIREWALL_IPTABLES: 'firewall.iptables',
161
- SECURENOW_FIREWALL_CLOUD: 'firewall.cloud',
162
- SECURENOW_FIREWALL_CLOUD_DRY_RUN: 'firewall.cloudDryRun',
163
- CLOUDFLARE_API_TOKEN: 'firewall.cloudflare.apiToken',
164
- CLOUDFLARE_ACCOUNT_ID: 'firewall.cloudflare.accountId',
165
- AWS_WAF_IP_SET_ID: 'firewall.aws.wafIpSetId',
166
- AWS_WAF_IP_SET_NAME: 'firewall.aws.wafIpSetName',
167
- AWS_WAF_SCOPE: 'firewall.aws.wafScope',
168
- GCP_PROJECT_ID: 'firewall.gcp.projectId',
169
- GCP_SECURITY_POLICY: 'firewall.gcp.securityPolicy',
170
- SECURENOW_TRUSTED_PROXIES: 'networking.trustedProxies',
171
- });
172
-
173
133
  function clone(value) {
174
134
  return value == null ? value : JSON.parse(JSON.stringify(value));
175
135
  }
@@ -460,7 +420,7 @@ function withCredentialDefaults(credentials) {
460
420
  out._securenow = mergeMissing(out._securenow, {
461
421
  schemaVersion: CONFIG_SCHEMA_VERSION,
462
422
  note: 'Local SecureNow credentials and secure SDK defaults. This file may contain secrets; keep .securenow/ in .gitignore.',
463
- precedence: `The same credentials file works in local development and production. SDK runtime config is read from credentials JSON. Legacy environment fallbacks are disabled unless ${LEGACY_ENV_FALLBACK_FLAG}=1 is set.`,
423
+ precedence: 'The same credentials file works in local development and production. SDK runtime config is read from credentials JSON only.',
464
424
  explanations: CONFIG_EXPLANATIONS,
465
425
  });
466
426
  return out;
@@ -476,19 +436,6 @@ function getPath(obj, dotted) {
476
436
  return cur;
477
437
  }
478
438
 
479
- function legacyEnvFallbackEnabled() {
480
- const raw =
481
- process.env[LEGACY_ENV_FALLBACK_FLAG] ??
482
- process.env.SECURENOW_ALLOW_ENV_CONFIG ??
483
- process.env.SECURENOW_LEGACY_ENV;
484
- return /^(1|true|yes)$/i.test(String(raw || '').trim());
485
- }
486
-
487
- function rawEnv(key) {
488
- if (!legacyEnvFallbackEnabled()) return undefined;
489
- return process.env[key] ?? process.env[key.toUpperCase()] ?? process.env[key.toLowerCase()];
490
- }
491
-
492
439
  function normalizeInstanceEndpoint(value) {
493
440
  if (value === undefined || value === null || value === '') return value;
494
441
  const endpoint = String(value).trim().replace(/\/$/, '');
@@ -626,24 +573,11 @@ function headersToString(headers) {
626
573
  .join(',');
627
574
  }
628
575
 
629
- function toEnvString(value) {
630
- if (value === undefined || value === null || value === '') return undefined;
631
- if (typeof value === 'boolean') return value ? '1' : '0';
632
- if (Array.isArray(value)) return value.join(',');
633
- if (typeof value === 'object') return headersToString(value);
634
- return String(value);
635
- }
636
-
637
- function resolveConfigPath(configPath, envKeys = [], fallback) {
576
+ function resolveConfigPath(configPath, _envKeys = [], fallback) {
638
577
  const creds = loadCredentials();
639
578
  const fromCreds = pick(getPath(creds && creds.config, configPath));
640
579
  if (fromCreds != null) return fromCreds;
641
580
 
642
- for (const key of envKeys) {
643
- const fromEnv = pick(rawEnv(key));
644
- if (fromEnv != null) return fromEnv;
645
- }
646
-
647
581
  const fromDefault = pick(getPath(DEFAULT_CONFIG, configPath));
648
582
  if (fromDefault != null) return fromDefault;
649
583
 
@@ -669,28 +603,12 @@ function listConfig(configPath) {
669
603
  function resolveAppKey() {
670
604
  const creds = loadCredentials();
671
605
  if (creds && creds.app && pick(creds.app.key)) return String(pick(creds.app.key));
672
-
673
- const fromEnv = pick(rawEnv('SECURENOW_APPID')) || pick(rawEnv('securenow'));
674
- if (fromEnv) return String(fromEnv);
675
-
676
- // Legacy compatibility: older docs sometimes used SECURENOW_API_KEY as the
677
- // app routing UUID. Real firewall keys start with snk_live_ and are handled
678
- // by resolveApiKey(), not as service.name.
679
- const legacyApiKey = pick(rawEnv('SECURENOW_API_KEY'));
680
- if (legacyApiKey && !String(legacyApiKey).startsWith('snk_live_')) {
681
- return String(legacyApiKey);
682
- }
683
-
684
606
  return null;
685
607
  }
686
608
 
687
609
  function resolveAppName() {
688
610
  const creds = loadCredentials();
689
611
  if (creds && creds.app && pick(creds.app.name)) return String(pick(creds.app.name));
690
-
691
- const fromEnv = pick(rawEnv('OTEL_SERVICE_NAME'));
692
- if (fromEnv) return String(fromEnv);
693
-
694
612
  return loadPackageJsonName();
695
613
  }
696
614
 
@@ -702,23 +620,12 @@ function resolveApiKey() {
702
620
  const creds = loadCredentials();
703
621
  const fromCreds = creds && pick(creds.apiKey);
704
622
  if (fromCreds && String(fromCreds).startsWith('snk_live_')) return String(fromCreds);
705
-
706
- const fromEnv = pick(rawEnv('SECURENOW_API_KEY'));
707
- if (fromEnv && String(fromEnv).startsWith('snk_live_')) return String(fromEnv);
708
-
709
623
  return null;
710
624
  }
711
625
 
712
626
  function resolveInstance() {
713
627
  const fromConfig = pick(resolveConfigPath('otel.endpoint'));
714
628
  if (fromConfig) return normalizeInstanceEndpoint(fromConfig);
715
-
716
- const fromEnv =
717
- pick(rawEnv('SECURENOW_INSTANCE')) ||
718
- pick(rawEnv('securenow_instance')) ||
719
- pick(rawEnv('OTEL_EXPORTER_OTLP_ENDPOINT'));
720
- if (fromEnv) return normalizeInstanceEndpoint(fromEnv);
721
-
722
629
  return FREE_TRIAL_INSTANCE;
723
630
  }
724
631
 
@@ -743,51 +650,18 @@ function resolveNoUuid(opts = {}) {
743
650
  const fromConfig = pick(getPath(loadCredentials()?.config, 'runtime.noUuid'));
744
651
  if (fromConfig !== null) return parseBool(fromConfig, false);
745
652
 
746
- const raw = pick(rawEnv('SECURENOW_NO_UUID'));
747
- if (raw !== null) return parseBool(raw, false);
748
-
749
653
  return !!resolveAppKey();
750
654
  }
751
655
 
752
- function resolveEnvKey(key) {
753
- const upper = String(key).toUpperCase();
754
-
755
- if (upper === 'SECURENOW_APPID') return resolveAppKey();
756
- if (upper === 'OTEL_SERVICE_NAME') return resolveAppName();
757
- if (upper === 'SECURENOW_API_KEY') return resolveApiKey();
758
- if (upper === 'SECURENOW_INSTANCE' || upper === 'OTEL_EXPORTER_OTLP_ENDPOINT') return resolveInstance();
759
- if (upper === 'SECURENOW_FIREWALL_ENABLED') return resolveFirewallEnabled();
760
-
761
- const configPath = ENV_TO_CONFIG_PATH[upper];
762
- if (configPath) return resolveConfigPath(configPath);
763
-
764
- const fromEnv = pick(rawEnv(upper));
765
- if (fromEnv != null) return fromEnv;
766
-
767
- return undefined;
768
- }
769
-
770
- function env(key) {
771
- return toEnvString(resolveEnvKey(key));
772
- }
773
-
774
- function boolEnv(key, fallback = false) {
775
- return parseBool(resolveEnvKey(key), fallback);
776
- }
777
-
778
- function numberEnv(key, fallback, min) {
779
- return parseNumber(resolveEnvKey(key), fallback, min);
780
- }
781
-
782
- function listEnv(key) {
783
- return parseList(resolveEnvKey(key));
784
- }
785
-
786
656
  function resolveOtlpHeaders() {
787
657
  const headers = parseHeaders(configValue('otel.headers', {}));
788
658
  const appKey = resolveAppKey();
789
- if (appKey && !headers['x-api-key']) {
790
- headers['x-api-key'] = appKey;
659
+ const apiKey = resolveApiKey();
660
+ if (appKey && !headers['x-securenow-app-key']) {
661
+ headers['x-securenow-app-key'] = appKey;
662
+ }
663
+ if (apiKey && !headers.authorization) {
664
+ headers.authorization = `Bearer ${apiKey}`;
791
665
  }
792
666
  const deploymentEnvironment = resolveDeploymentEnvironment();
793
667
  if (deploymentEnvironment && !headers['x-securenow-environment']) {
@@ -855,14 +729,12 @@ module.exports = {
855
729
  DEFAULT_API_URL,
856
730
  DEFAULT_FIREWALL_API_URL,
857
731
  LEGACY_SECURENOW_GATEWAY,
858
- LEGACY_ENV_FALLBACK_FLAG,
859
732
  CONFIG_SCHEMA_VERSION,
860
733
  CREDENTIAL_FILE_ENVIRONMENTS,
861
734
  RUNTIME_CREDENTIALS_FILENAME,
862
735
  ADMIN_CREDENTIALS_FILENAME,
863
736
  DEFAULT_CONFIG,
864
737
  CONFIG_EXPLANATIONS,
865
- ENV_TO_CONFIG_PATH,
866
738
  withCredentialDefaults,
867
739
  mergeCredentials,
868
740
  resolveConfigPath,
@@ -876,7 +748,6 @@ module.exports = {
876
748
  resolveApiKey,
877
749
  resolveInstance,
878
750
  normalizeDeploymentEnvironment,
879
- legacyEnvFallbackEnabled,
880
751
  resolveDeploymentEnvironment,
881
752
  resolveAll,
882
753
  resolveNoUuid,
@@ -888,10 +759,6 @@ module.exports = {
888
759
  resolveFirewallApiUrl,
889
760
  resolveFirewallApiFallbacks,
890
761
  resolveFirewallOptions,
891
- env,
892
- boolEnv,
893
- numberEnv,
894
- listEnv,
895
762
  parseHeaders,
896
763
  normalizeInstanceEndpoint,
897
764
  normalizeSignalEndpoint,
package/cli/apiKey.js CHANGED
@@ -4,6 +4,10 @@ const { api, requireAuth } = require('./client');
4
4
  const config = require('./config');
5
5
  const ui = require('./ui');
6
6
 
7
+ const PRESET_SCOPES = {
8
+ runtime_app: ['traces:write', 'logs:write', 'firewall:read', 'blocklist:read', 'allowlist:read'],
9
+ };
10
+
7
11
  function maskKey(key) {
8
12
  if (!key || key.length < 16) return key || '';
9
13
  return `${key.slice(0, 12)}••••••${key.slice(-4)}`;
@@ -12,16 +16,27 @@ function maskKey(key) {
12
16
  async function create(args, flags) {
13
17
  requireAuth();
14
18
 
15
- const name = flags.name || args.join(' ').trim() || 'CLI firewall';
16
- const preset = flags.preset || 'firewall';
19
+ const name = flags.name || args.join(' ').trim() || 'CLI runtime';
20
+ const preset = flags.preset || 'runtime_app';
17
21
  const local = flags.global ? false : true;
22
+ const app = config.getApp();
18
23
 
19
24
  const s = ui.spinner(`Creating ${preset} API key`);
20
25
  try {
21
- const result = await api.post('/api-keys', { name, preset });
26
+ const body = { name, preset };
27
+ if (PRESET_SCOPES[preset]) {
28
+ body.scopes = PRESET_SCOPES[preset];
29
+ }
30
+ if (flags.app) {
31
+ body.apps = [flags.app];
32
+ } else if (preset === 'runtime_app' && app?.key) {
33
+ body.apps = [app.key];
34
+ }
35
+
36
+ const result = await api.post('/api-keys', body);
22
37
  const key = result && result.key;
23
38
  if (!key || !key.startsWith('snk_live_')) {
24
- throw new Error('API did not return a valid firewall key');
39
+ throw new Error('API did not return a valid runtime key');
25
40
  }
26
41
 
27
42
  config.setApiKey(key, { local });
@@ -34,6 +49,7 @@ async function create(args, flags) {
34
49
  name: result.apiKey?.name || name,
35
50
  preset,
36
51
  key: maskKey(key),
52
+ appScope: body.apps || [],
37
53
  savedTo: local ? 'project .securenow/runtime.json' : '~/.securenow/runtime.json',
38
54
  });
39
55
  return;
@@ -43,7 +59,7 @@ async function create(args, flags) {
43
59
  ui.info(local
44
60
  ? 'Stored in project .securenow/runtime.json (local)'
45
61
  : 'Stored in ~/.securenow/runtime.json (global)');
46
- ui.info('The plaintext key is not printed. The SDK will read it from the credentials file.');
62
+ ui.info('The plaintext key is not printed. The SDK will read it from the runtime credentials file.');
47
63
  } catch (err) {
48
64
  s.fail('Failed to create API key');
49
65
  throw err;
@@ -87,7 +103,7 @@ async function clear(args, flags) {
87
103
  async function show() {
88
104
  const key = config.getApiKey();
89
105
  if (!key) {
90
- ui.info('Runtime firewall enforcement key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` to refresh .securenow/runtime.json.');
106
+ ui.info('Runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
91
107
  return;
92
108
  }
93
109
  console.log(maskKey(key));
package/cli/auth.js CHANGED
@@ -275,7 +275,7 @@ async function login(args, flags) {
275
275
  ui.info('Runtime app config was not changed.');
276
276
  }
277
277
  if (apiKey) {
278
- ui.info(`Firewall API key saved — the firewall will activate automatically on next start`);
278
+ ui.info(`Runtime API key saved — telemetry ingestion and firewall sync will authenticate automatically on next start`);
279
279
  }
280
280
  if (exp) {
281
281
  const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
@@ -343,9 +343,9 @@ async function appConnect(args, flags) {
343
343
  ui.success(`Runtime app connected to ${ui.c.bold(app.name || app.key)}`);
344
344
  ui.info(local ? 'Saved to project .securenow/runtime.json' : 'Saved to ~/.securenow/runtime.json');
345
345
  if (apiKey) {
346
- ui.info('Firewall API key saved; SDK runtime can enforce firewall without an admin token.');
346
+ ui.info('Runtime API key saved; SDK telemetry and firewall sync can run without an admin token.');
347
347
  } else {
348
- ui.warn('No firewall API key was returned. Run `securenow api-key create` if firewall sync needs a key.');
348
+ ui.warn('No runtime API key was returned. Run `securenow api-key create` before sending telemetry.');
349
349
  }
350
350
  ui.info('Admin auth was not changed.');
351
351
  }
@@ -393,7 +393,7 @@ async function whoami() {
393
393
  ['Status', runtime.app?.key ? ui.c.green('connected') : ui.c.red('no app selected')],
394
394
  ['App', runtime.app?.name || '—'],
395
395
  ['App Key', runtime.app?.key || '—'],
396
- ['Firewall Key', runtime.apiKey ? ui.c.green('present') : ui.c.yellow('missing')],
396
+ ['Runtime API Key', runtime.apiKey ? ui.c.green('present') : ui.c.yellow('missing')],
397
397
  ['Environment', runtime.config?.runtime?.deploymentEnvironment || 'production'],
398
398
  ['Runtime Source', config.getRuntimeSource()],
399
399
  ['API', config.getApiUrl()],
package/cli/config.js CHANGED
@@ -127,7 +127,6 @@ function resolveRuntimeCredentialsFile() {
127
127
  }
128
128
 
129
129
  function getAuthSource() {
130
- if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
131
130
  const file = appConfig.resolveLocalAdminCredentialsFile() || appConfig.resolveGlobalAdminCredentialsFile();
132
131
  if (!file) return 'not configured';
133
132
  if (file.includes(`${path.sep}.securenow${path.sep}`) && file.startsWith(process.cwd())) {
@@ -188,9 +187,6 @@ function saveCredentials(creds, { local = false } = {}) {
188
187
  }
189
188
 
190
189
  function loadAdminCredentials() {
191
- if (process.env.SECURENOW_TOKEN) {
192
- return { token: process.env.SECURENOW_TOKEN, source: 'env' };
193
- }
194
190
  const adminFile = pickExistingFile(
195
191
  appConfig.resolveLocalAdminCredentialsFile(),
196
192
  appConfig.resolveGlobalAdminCredentialsFile()
@@ -305,8 +301,6 @@ function clearCredentials({ local } = {}) {
305
301
  }
306
302
 
307
303
  function getToken() {
308
- if (process.env.SECURENOW_TOKEN) return process.env.SECURENOW_TOKEN;
309
-
310
304
  const creds = loadAdminCredentials();
311
305
  if (!creds.token) return null;
312
306
 
@@ -412,18 +406,17 @@ function ensureLocalGitignore() {
412
406
  }
413
407
 
414
408
  function getApiUrl() {
415
- if (process.env.SECURENOW_API_URL) return process.env.SECURENOW_API_URL;
416
409
  const cfg = loadConfig();
417
410
  if (cfg.apiUrl && cfg.apiUrl !== DEFAULTS.apiUrl) return cfg.apiUrl;
418
- return appConfig.env('SECURENOW_API_URL') || cfg.apiUrl;
411
+ return cfg.apiUrl;
419
412
  }
420
413
 
421
414
  function getAppUrl() {
422
- return process.env.SECURENOW_APP_URL || loadConfig().appUrl;
415
+ return loadConfig().appUrl;
423
416
  }
424
417
 
425
418
  function getDefaultApp() {
426
- return process.env.SECURENOW_APP || loadConfig().defaultApp;
419
+ return loadConfig().defaultApp;
427
420
  }
428
421
 
429
422
  module.exports = {
@@ -70,7 +70,7 @@ async function runtime(_args, flags) {
70
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 app connect` 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
 
@@ -500,9 +500,9 @@ async function doctor(_args, flags) {
500
500
  warnings.push('Using the legacy free-trial collector directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
501
501
  }
502
502
  if (!cfg.apiKey && token) {
503
- warnings.push('Admin CLI/MCP auth is connected, but runtime firewall enforcement key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` to refresh .securenow/runtime.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.');
504
504
  } else if (!cfg.apiKey) {
505
- warnings.push('Runtime firewall enforcement key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` to refresh .securenow/runtime.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.');
506
506
  }
507
507
 
508
508
  const ok = checks.every((c) => c.ok);
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/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/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
@@ -91,7 +91,7 @@ const COMMANDS = {
91
91
  usage: 'securenow app <subcommand> [options]',
92
92
  sub: {
93
93
  connect: {
94
- desc: 'Select/create app, mint firewall key, and write SDK runtime config',
94
+ desc: 'Select/create app, mint runtime API key, and write SDK runtime config',
95
95
  usage: 'securenow app connect [--global]',
96
96
  flags: { global: 'Save runtime config to ~/.securenow/runtime.json' },
97
97
  run: (a, f) => require('./cli/auth').appConnect(a, f),
@@ -109,17 +109,18 @@ const COMMANDS = {
109
109
  desc: 'Show admin auth and SDK runtime status',
110
110
  usage: 'securenow whoami',
111
111
  run: () => require('./cli/auth').whoami(),
112
- },
112
+ },
113
113
  'api-key': {
114
- desc: 'Manage the firewall API key stored in runtime credentials',
114
+ desc: 'Manage the runtime API key stored in runtime credentials',
115
115
  usage: 'securenow api-key <subcommand> [options]',
116
116
  sub: {
117
117
  create: {
118
- desc: 'Create a firewall API key with your session token and save it',
119
- 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]',
120
120
  flags: {
121
121
  name: 'Human-readable key name',
122
- 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',
123
124
  global: 'Save to ~/.securenow/ instead of project-local',
124
125
  },
125
126
  run: (a, f) => require('./cli/apiKey').create(a, f),
@@ -771,12 +772,9 @@ async function main() {
771
772
  }
772
773
  }
773
774
 
774
- main().catch((err) => {
775
- if (err.name !== 'CLIError') {
776
- ui.error(err.message || 'An unexpected error occurred');
777
- }
778
- if (process.env.SECURENOW_DEBUG) {
779
- console.error(err.stack || err);
780
- }
781
- process.exit(1);
782
- });
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,13 +7,10 @@
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