securenow 7.5.1 → 7.6.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.
Files changed (50) hide show
  1. package/CONSUMING-APPS-GUIDE.md +2 -0
  2. package/NPM_README.md +201 -237
  3. package/README.md +73 -26
  4. package/SKILL-API.md +209 -205
  5. package/SKILL-CLI.md +71 -64
  6. package/app-config.js +479 -83
  7. package/cli/apiKey.js +1 -1
  8. package/cli/apps.js +1 -1
  9. package/cli/config.js +31 -12
  10. package/cli/credentials.js +88 -0
  11. package/cli/diagnostics.js +68 -104
  12. package/cli/firewall.js +29 -14
  13. package/cli/init.js +208 -206
  14. package/cli/monitor.js +107 -43
  15. package/cli/security.js +24 -12
  16. package/cli/utils.js +2 -1
  17. package/cli.js +71 -39
  18. package/console-instrumentation.js +1 -1
  19. package/docs/ENVIRONMENT-VARIABLES.md +137 -863
  20. package/docs/ENVIRONMENTS.md +60 -0
  21. package/docs/EXPRESS-SETUP-GUIDE.md +3 -0
  22. package/docs/FIREWALL-GUIDE.md +3 -0
  23. package/docs/INDEX.md +6 -8
  24. package/docs/LOGGING-GUIDE.md +3 -0
  25. package/docs/MCP-GUIDE.md +8 -0
  26. package/docs/NEXTJS-GUIDE.md +3 -0
  27. package/docs/NEXTJS-QUICKSTART.md +24 -16
  28. package/docs/NUXT-GUIDE.md +3 -0
  29. package/docs/QUICKSTART-BODY-CAPTURE.md +3 -0
  30. package/docs/REQUEST-BODY-CAPTURE.md +3 -0
  31. package/firewall-cloud.js +10 -10
  32. package/firewall-only.js +25 -23
  33. package/firewall.js +47 -29
  34. package/free-trial-banner.js +1 -1
  35. package/mcp/catalog.js +104 -17
  36. package/nextjs-auto-capture.d.ts +7 -4
  37. package/nextjs-auto-capture.js +7 -7
  38. package/nextjs-middleware.js +4 -3
  39. package/nextjs-wrapper.js +6 -6
  40. package/nextjs.d.ts +36 -25
  41. package/nextjs.js +47 -55
  42. package/nuxt-server-plugin.mjs +35 -51
  43. package/nuxt.d.ts +29 -23
  44. package/package.json +1 -1
  45. package/postinstall.js +27 -61
  46. package/register.d.ts +19 -33
  47. package/register.js +8 -8
  48. package/resolve-ip.js +4 -5
  49. package/tracing.d.ts +21 -19
  50. package/tracing.js +34 -42
package/cli/config.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const appConfig = require('../app-config');
6
7
 
7
8
  const CONFIG_DIR = path.join(os.homedir(), '.securenow');
8
9
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
@@ -44,6 +45,10 @@ function saveJSON(filepath, data) {
44
45
  }
45
46
  }
46
47
 
48
+ function credentialsFileForLocal(local) {
49
+ return local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
50
+ }
51
+
47
52
  function hasLocalCredentials() {
48
53
  return fs.existsSync(LOCAL_CREDENTIALS_FILE);
49
54
  }
@@ -86,12 +91,21 @@ function setConfigValue(key, value) {
86
91
  }
87
92
 
88
93
  function loadCredentials() {
89
- return loadJSON(resolveCredentialsFile());
94
+ const global = loadJSON(CREDENTIALS_FILE);
95
+ const local = fs.existsSync(LOCAL_CREDENTIALS_FILE) ? loadJSON(LOCAL_CREDENTIALS_FILE) : null;
96
+ return appConfig.withCredentialDefaults(appConfig.mergeCredentials(global, local)) || {};
90
97
  }
91
98
 
92
99
  function saveCredentials(creds, { local = false } = {}) {
93
- const targetFile = local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
94
- saveJSON(targetFile, creds);
100
+ const targetFile = credentialsFileForLocal(local);
101
+ saveJSON(targetFile, appConfig.withCredentialDefaults(creds) || {});
102
+ }
103
+
104
+ function ensureCredentialDefaults({ local } = {}) {
105
+ const useLocal = local === true || (local == null && hasLocalCredentials());
106
+ const targetFile = credentialsFileForLocal(useLocal);
107
+ const existing = loadJSON(targetFile);
108
+ saveJSON(targetFile, appConfig.withCredentialDefaults(existing || {}) || {});
95
109
  }
96
110
 
97
111
  function clearCredentials({ local } = {}) {
@@ -119,7 +133,8 @@ function getToken() {
119
133
  }
120
134
 
121
135
  function setAuth(token, email, expiresAt, { local = false, app = null } = {}) {
122
- const payload = { token, email, expiresAt };
136
+ const targetFile = credentialsFileForLocal(local);
137
+ const payload = { ...loadJSON(targetFile), token, email, expiresAt };
123
138
  if (app && (app.key || app.name || app.instance)) {
124
139
  payload.app = {
125
140
  key: app.key || null,
@@ -137,18 +152,18 @@ function getApp() {
137
152
 
138
153
  function setApiKey(apiKey, { local } = {}) {
139
154
  const useLocal = local === true || (local == null && hasLocalCredentials());
140
- const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
155
+ const targetFile = credentialsFileForLocal(useLocal);
141
156
  const existing = loadJSON(targetFile);
142
- saveJSON(targetFile, { ...existing, apiKey });
157
+ saveJSON(targetFile, appConfig.withCredentialDefaults({ ...existing, apiKey }) || { apiKey });
143
158
  }
144
159
 
145
160
  function clearApiKey({ local } = {}) {
146
161
  const useLocal = local === true || (local == null && hasLocalCredentials());
147
- const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
162
+ const targetFile = credentialsFileForLocal(useLocal);
148
163
  const existing = loadJSON(targetFile);
149
164
  if (!existing || !existing.apiKey) return;
150
165
  delete existing.apiKey;
151
- saveJSON(targetFile, existing);
166
+ saveJSON(targetFile, appConfig.withCredentialDefaults(existing) || existing);
152
167
  }
153
168
 
154
169
  function getApiKey() {
@@ -158,16 +173,16 @@ function getApiKey() {
158
173
 
159
174
  function setApp(app, { local } = {}) {
160
175
  const useLocal = local === true || (local == null && hasLocalCredentials());
161
- const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
176
+ const targetFile = credentialsFileForLocal(useLocal);
162
177
  const existing = loadJSON(targetFile);
163
- saveJSON(targetFile, {
178
+ saveJSON(targetFile, appConfig.withCredentialDefaults({
164
179
  ...existing,
165
180
  app: {
166
181
  key: app.key || null,
167
182
  name: app.name || null,
168
183
  instance: app.instance || null,
169
184
  },
170
- });
185
+ }) || {});
171
186
  }
172
187
 
173
188
  function ensureLocalGitignore() {
@@ -186,7 +201,10 @@ function ensureLocalGitignore() {
186
201
  }
187
202
 
188
203
  function getApiUrl() {
189
- return process.env.SECURENOW_API_URL || loadConfig().apiUrl;
204
+ if (process.env.SECURENOW_API_URL) return process.env.SECURENOW_API_URL;
205
+ const cfg = loadConfig();
206
+ if (cfg.apiUrl && cfg.apiUrl !== DEFAULTS.apiUrl) return cfg.apiUrl;
207
+ return appConfig.env('SECURENOW_API_URL') || cfg.apiUrl;
190
208
  }
191
209
 
192
210
  function getAppUrl() {
@@ -219,6 +237,7 @@ module.exports = {
219
237
  getApiKey,
220
238
  getAuthSource,
221
239
  hasLocalCredentials,
240
+ ensureCredentialDefaults,
222
241
  ensureLocalGitignore,
223
242
  getApiUrl,
224
243
  getAppUrl,
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const config = require('./config');
6
+ const ui = require('./ui');
7
+ const appConfig = require('../app-config');
8
+
9
+ function maskSecret(value) {
10
+ if (!value) return '';
11
+ const text = String(value);
12
+ if (text.length <= 12) return '***';
13
+ return `${text.slice(0, 8)}...${text.slice(-4)}`;
14
+ }
15
+
16
+ function buildRuntimeCredentials(options = {}) {
17
+ const creds = config.loadCredentials() || {};
18
+ const deploymentEnvironment =
19
+ options.environment ||
20
+ options.env ||
21
+ appConfig.resolveDeploymentEnvironment() ||
22
+ 'production';
23
+ const runtime = appConfig.withCredentialDefaults({
24
+ apiKey: creds.apiKey || null,
25
+ app: {
26
+ key: creds.app?.key || null,
27
+ name: creds.app?.name || null,
28
+ instance: creds.app?.instance || appConfig.FREE_TRIAL_INSTANCE,
29
+ },
30
+ config: {
31
+ ...(creds.config || {}),
32
+ runtime: {
33
+ ...(creds.config?.runtime || {}),
34
+ deploymentEnvironment,
35
+ },
36
+ },
37
+ _securenow: {
38
+ ...(creds._securenow || {}),
39
+ note: 'Runtime SecureNow credentials and SDK defaults. Mount or copy this JSON as .securenow/credentials.json in production. Do not commit it.',
40
+ runtimeOnly: 'This file intentionally omits CLI OAuth fields: token, email, and expiresAt.',
41
+ production: 'Production can use this same file shape instead of environment variables.',
42
+ },
43
+ });
44
+
45
+ delete runtime.token;
46
+ delete runtime.email;
47
+ delete runtime.expiresAt;
48
+ return runtime;
49
+ }
50
+
51
+ function writeFileSafe(filepath, data) {
52
+ fs.mkdirSync(path.dirname(filepath), { recursive: true, mode: 0o700 });
53
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
54
+ }
55
+
56
+ async function runtime(_args, flags) {
57
+ const creds = buildRuntimeCredentials(flags);
58
+ const envName = creds.config?.runtime?.deploymentEnvironment || 'production';
59
+ const defaultOutput = envName === 'production'
60
+ ? path.join('.securenow', 'credentials.production.json')
61
+ : path.join('.securenow', `credentials.${envName}.json`);
62
+ const output = flags.out || flags.output || defaultOutput;
63
+ const warn = flags.stdout ? (msg) => console.error(`! ${msg}`) : ui.warn;
64
+
65
+ if (!creds.app || !creds.app.key) {
66
+ warn('No app key found. Run `npx securenow login` first so telemetry routes to the selected app.');
67
+ }
68
+ if (!creds.apiKey) {
69
+ warn('No firewall API key found. Run `npx securenow login` or `npx securenow api-key set snk_live_...` first.');
70
+ }
71
+
72
+ if (flags.stdout) {
73
+ console.log(JSON.stringify(creds, null, 2));
74
+ return;
75
+ }
76
+
77
+ writeFileSafe(path.resolve(process.cwd(), output), creds);
78
+ config.ensureLocalGitignore();
79
+
80
+ ui.success(`Wrote runtime credentials to ${output}`);
81
+ ui.info('Deploy this JSON as .securenow/credentials.json on the server/container.');
82
+ ui.info(`Environment: ${envName}`);
83
+ ui.info(`App: ${creds.app?.name || '(unnamed)'} ${creds.app?.key ? `(${creds.app.key})` : ''}`);
84
+ ui.info(`Firewall key: ${creds.apiKey ? maskSecret(creds.apiKey) : '(missing)'}`);
85
+ ui.info('OAuth token/email were not included.');
86
+ }
87
+
88
+ module.exports = { runtime, buildRuntimeCredentials };
@@ -4,62 +4,43 @@ const crypto = require('crypto');
4
4
  const url = require('url');
5
5
  const ui = require('./ui');
6
6
  const config = require('./config');
7
+ const appConfig = require('../app-config');
7
8
 
8
- // ── Config resolution (mirrors tracing.js priority order) ──
9
-
10
- function resolvedConfig() {
11
- // Mirror the SDK's resolution: env → credentials file → package.json#name.
12
- // Doing it here means `securenow doctor` reports what the SDK will actually
13
- // send — critical for diagnosing "my traces don't show up" (the common
14
- // cause is a service.name mismatch with the dashboard's exact-match filter).
15
- const appConfig = require('../app-config');
9
+ function resolvedConfig(options = {}) {
16
10
  const resolvedApp = appConfig.resolveAll();
11
+ const endpoints = appConfig.resolveEndpoints();
12
+ const firewall = appConfig.resolveFirewallOptions();
17
13
  const noUuid = appConfig.resolveNoUuid();
14
+ const deploymentEnvironment = appConfig.normalizeDeploymentEnvironment(
15
+ options.environment || options.env || resolvedApp.deploymentEnvironment
16
+ );
18
17
 
19
- const baseName = (process.env.OTEL_SERVICE_NAME || resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '') || null;
18
+ const baseName = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '') || null;
20
19
  const serviceName = baseName
21
20
  ? (noUuid ? baseName : `${baseName}-<uuid-per-worker>`)
22
21
  : '(auto-generated)';
23
- const instance =
24
- resolvedApp.instance || 'https://freetrial.securenow.ai:4318';
25
- const tracesEndpoint =
26
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
27
- (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
28
- ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/traces`
29
- : `${instance.replace(/\/$/, '')}/v1/traces`);
30
- const logsEndpoint =
31
- process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ||
32
- (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
33
- ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/logs`
34
- : `${instance.replace(/\/$/, '')}/v1/logs`);
35
- const headers = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
36
- // Resolve firewall API key the same way the SDK does: env, then
37
- // .securenow/credentials.json (project-local, then global).
38
- const apiKey = require('../app-config').resolveApiKey() || '';
39
- const apiUrl = config.getApiUrl();
40
- const loggingEnabled = !/^(0|false)$/i.test(String(process.env.SECURENOW_LOGGING_ENABLED ?? ''));
41
- const captureBody = !/^(0|false)$/i.test(String(process.env.SECURENOW_CAPTURE_BODY ?? ''));
42
- const captureMultipart = !/^(0|false)$/i.test(String(process.env.SECURENOW_CAPTURE_MULTIPART ?? ''));
43
- const firewallEnabled =
44
- !!apiKey && process.env.SECURENOW_FIREWALL_ENABLED !== '0';
22
+ const apiKey = firewall.apiKey || '';
23
+ const firewallEnabled = !!apiKey && firewall.enabled;
45
24
 
46
25
  return {
47
26
  serviceName,
48
- instance,
49
- tracesEndpoint,
50
- logsEndpoint,
51
- headers,
27
+ appKey: resolvedApp.appKey || null,
28
+ deploymentEnvironment,
29
+ instance: endpoints.endpointBase,
30
+ tracesEndpoint: endpoints.tracesUrl,
31
+ logsEndpoint: endpoints.logsUrl,
32
+ headers: endpoints.headers,
52
33
  apiKey,
53
- apiUrl,
54
- loggingEnabled,
55
- captureBody,
56
- captureMultipart,
34
+ apiUrl: config.getApiUrl(),
35
+ loggingEnabled: appConfig.boolEnv('SECURENOW_LOGGING_ENABLED', true),
36
+ captureBody: appConfig.boolEnv('SECURENOW_CAPTURE_BODY', true),
37
+ captureMultipart: appConfig.boolEnv('SECURENOW_CAPTURE_MULTIPART', true),
57
38
  firewallEnabled,
58
39
  firewallLayers: {
59
40
  http: firewallEnabled,
60
- tcp: firewallEnabled && process.env.SECURENOW_FIREWALL_TCP === '1',
61
- iptables: firewallEnabled && process.env.SECURENOW_FIREWALL_IPTABLES === '1',
62
- cloud: firewallEnabled ? process.env.SECURENOW_FIREWALL_CLOUD || null : null,
41
+ tcp: firewallEnabled && firewall.tcp,
42
+ iptables: firewallEnabled && firewall.iptables,
43
+ cloud: firewallEnabled ? firewall.cloud : null,
63
44
  },
64
45
  };
65
46
  }
@@ -74,24 +55,11 @@ function maskSecret(value) {
74
55
  function redactConfig(cfg) {
75
56
  return {
76
57
  ...cfg,
77
- headers: cfg.headers ? '***' : '',
58
+ headers: cfg.headers && Object.keys(cfg.headers).length ? '***' : '',
78
59
  apiKey: cfg.apiKey ? maskSecret(cfg.apiKey) : '',
79
60
  };
80
61
  }
81
62
 
82
- function parseHeaders(str) {
83
- const out = {};
84
- if (!str) return out;
85
- for (const pair of str.split(',')) {
86
- const [k, ...rest] = pair.split('=');
87
- if (!k) continue;
88
- out[k.trim()] = rest.join('=').trim();
89
- }
90
- return out;
91
- }
92
-
93
- // ── HTTP helpers ──
94
-
95
63
  function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs = 5000 }) {
96
64
  return new Promise((resolve, reject) => {
97
65
  const parsed = new url.URL(endpoint);
@@ -121,8 +89,6 @@ function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs
121
89
  });
122
90
  }
123
91
 
124
- // ── OTLP/HTTP JSON payloads ──
125
-
126
92
  function attr(key, value) {
127
93
  if (typeof value === 'number') {
128
94
  if (Number.isInteger(value)) return { key, value: { intValue: String(value) } };
@@ -135,7 +101,7 @@ function attr(key, value) {
135
101
  function resourceAttrs(cfg, extra = {}) {
136
102
  const base = {
137
103
  'service.name': cfg.serviceName,
138
- 'deployment.environment': process.env.NODE_ENV || 'development',
104
+ 'deployment.environment': cfg.deploymentEnvironment || appConfig.resolveDeploymentEnvironment(),
139
105
  'telemetry.sdk.name': 'securenow-cli',
140
106
  'telemetry.sdk.language': 'nodejs',
141
107
  ...extra,
@@ -201,18 +167,19 @@ function buildLogPayload(cfg, { message, level = 'info', attributes = {} }) {
201
167
  };
202
168
  }
203
169
 
204
- // ── Commands ──
205
-
206
170
  async function testSpan(args, flags) {
207
- const cfg = resolvedConfig();
171
+ const cfg = resolvedConfig(flags);
208
172
  const spanName = args[0] || 'securenow.cli.test-span';
209
173
  const headers = {
210
174
  'Content-Type': 'application/json',
211
- ...parseHeaders(cfg.headers),
175
+ ...cfg.headers,
212
176
  };
213
177
  const payload = buildTracePayload(cfg, {
214
178
  name: spanName,
215
- attributes: { 'test.source': 'securenow-cli' },
179
+ attributes: {
180
+ 'test.source': 'securenow-cli',
181
+ 'securenow.environment': cfg.deploymentEnvironment,
182
+ },
216
183
  });
217
184
 
218
185
  const spin = ui.spinner(`Sending test span to ${cfg.tracesEndpoint}`);
@@ -224,7 +191,7 @@ async function testSpan(args, flags) {
224
191
  });
225
192
  if (res.status >= 200 && res.status < 300) {
226
193
  spin.stop(`Span accepted (HTTP ${res.status})`);
227
- if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.tracesEndpoint });
194
+ if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.tracesEndpoint, environment: cfg.deploymentEnvironment });
228
195
  return;
229
196
  }
230
197
  spin.fail(`Collector returned HTTP ${res.status}`);
@@ -246,9 +213,9 @@ async function logSend(args, flags) {
246
213
  process.exit(1);
247
214
  }
248
215
 
249
- const cfg = resolvedConfig();
216
+ const cfg = resolvedConfig(flags);
250
217
  if (!cfg.loggingEnabled) {
251
- ui.warn('Logging is disabled (SECURENOW_LOGGING_ENABLED=0). Sending anyway.');
218
+ ui.warn('Logging is disabled in SecureNow credentials/config. Sending anyway.');
252
219
  }
253
220
 
254
221
  const level = (flags.level || 'info').toString();
@@ -262,9 +229,16 @@ async function logSend(args, flags) {
262
229
 
263
230
  const headers = {
264
231
  'Content-Type': 'application/json',
265
- ...parseHeaders(cfg.headers),
232
+ ...cfg.headers,
266
233
  };
267
- const payload = buildLogPayload(cfg, { message, level, attributes });
234
+ const payload = buildLogPayload(cfg, {
235
+ message,
236
+ level,
237
+ attributes: {
238
+ ...attributes,
239
+ 'securenow.environment': cfg.deploymentEnvironment,
240
+ },
241
+ });
268
242
 
269
243
  const spin = ui.spinner(`Sending log to ${cfg.logsEndpoint}`);
270
244
  try {
@@ -275,7 +249,7 @@ async function logSend(args, flags) {
275
249
  });
276
250
  if (res.status >= 200 && res.status < 300) {
277
251
  spin.stop(`Log accepted (HTTP ${res.status})`);
278
- if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.logsEndpoint });
252
+ if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.logsEndpoint, environment: cfg.deploymentEnvironment });
279
253
  return;
280
254
  }
281
255
  spin.fail(`Collector returned HTTP ${res.status}`);
@@ -289,12 +263,8 @@ async function logSend(args, flags) {
289
263
  }
290
264
  }
291
265
 
292
- // ── doctor / env ──
293
-
294
266
  async function probe(endpoint, timeoutMs = 3000) {
295
267
  try {
296
- // A HEAD or empty POST is safer than sending real OTLP. Most collectors
297
- // return 400/405 for a malformed request — that still proves reachability.
298
268
  const res = await httpRequest({
299
269
  method: 'POST',
300
270
  endpoint,
@@ -310,34 +280,32 @@ async function probe(endpoint, timeoutMs = 3000) {
310
280
 
311
281
  function env(_args, flags) {
312
282
  const cfg = resolvedConfig();
313
- const vars = {
314
- SECURENOW_APPID: process.env.SECURENOW_APPID || null,
315
- OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME || null,
316
- SECURENOW_INSTANCE: process.env.SECURENOW_INSTANCE || null,
317
- OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || null,
318
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || null,
319
- OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || null,
320
- OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS ? '***' : null,
321
- SECURENOW_API_KEY: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
322
- SECURENOW_API_URL: cfg.apiUrl,
323
- SECURENOW_LOGGING_ENABLED: cfg.loggingEnabled ? '1' : '0',
324
- SECURENOW_CAPTURE_BODY: cfg.captureBody ? '1' : '0',
325
- SECURENOW_CAPTURE_MULTIPART: cfg.captureMultipart ? '1' : '0',
326
- SECURENOW_NO_UUID: process.env.SECURENOW_NO_UUID || `(auto: ${require('../app-config').resolveNoUuid() ? '1' : '0'})`,
327
- SECURENOW_FIREWALL_TCP: process.env.SECURENOW_FIREWALL_TCP || '0',
328
- SECURENOW_FIREWALL_IPTABLES: process.env.SECURENOW_FIREWALL_IPTABLES || '0',
329
- SECURENOW_FIREWALL_CLOUD: process.env.SECURENOW_FIREWALL_CLOUD || null,
330
- NODE_ENV: process.env.NODE_ENV || 'development',
283
+ const resolved = {
284
+ appKey: cfg.appKey,
285
+ serviceName: cfg.serviceName,
286
+ deploymentEnvironment: cfg.deploymentEnvironment,
287
+ instance: cfg.instance,
288
+ tracesEndpoint: cfg.tracesEndpoint,
289
+ logsEndpoint: cfg.logsEndpoint,
290
+ otlpHeaders: Object.keys(cfg.headers || {}).length ? '***' : null,
291
+ firewallApiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
292
+ apiUrl: cfg.apiUrl,
293
+ loggingEnabled: cfg.loggingEnabled,
294
+ captureBody: cfg.captureBody,
295
+ captureMultipart: cfg.captureMultipart,
296
+ noUuid: appConfig.resolveNoUuid(),
297
+ firewallLayers: cfg.firewallLayers,
331
298
  };
332
299
 
333
300
  if (flags.json) {
334
- ui.json({ resolved: redactConfig(cfg), env: vars });
301
+ ui.json({ resolved: redactConfig(cfg), config: resolved });
335
302
  return;
336
303
  }
337
304
 
338
305
  ui.heading('Resolved configuration');
339
306
  ui.keyValue([
340
307
  ['Service name', cfg.serviceName],
308
+ ['Environment', cfg.deploymentEnvironment],
341
309
  ['Traces endpoint', cfg.tracesEndpoint],
342
310
  ['Logs endpoint', cfg.logsEndpoint],
343
311
  ['Logging', cfg.loggingEnabled ? ui.c.green('enabled') : ui.c.dim('disabled')],
@@ -346,9 +314,9 @@ function env(_args, flags) {
346
314
  ['Firewall', cfg.firewallEnabled ? ui.c.green('enabled') : ui.c.dim('disabled (no API key)')],
347
315
  ]);
348
316
 
349
- ui.heading('Environment variables');
317
+ ui.heading('Resolved credentials');
350
318
  ui.keyValue(
351
- Object.entries(vars).map(([k, v]) => [k, v == null ? ui.c.dim('(not set)') : String(v)])
319
+ Object.entries(resolved).map(([k, v]) => [k, v == null ? ui.c.dim('(not set)') : typeof v === 'object' ? JSON.stringify(v) : String(v)])
352
320
  );
353
321
  console.log('');
354
322
  }
@@ -357,21 +325,18 @@ async function doctor(_args, flags) {
357
325
  const cfg = resolvedConfig();
358
326
  const checks = [];
359
327
 
360
- // Collector traces
361
328
  const spin1 = ui.spinner(`Probing traces endpoint ${cfg.tracesEndpoint}`);
362
329
  const traces = await probe(cfg.tracesEndpoint);
363
330
  if (traces.ok) spin1.stop(`Traces endpoint reachable (HTTP ${traces.status})`);
364
331
  else spin1.fail(`Traces endpoint unreachable: ${traces.error}`);
365
332
  checks.push({ name: 'traces', ...traces });
366
333
 
367
- // Collector logs
368
334
  const spin2 = ui.spinner(`Probing logs endpoint ${cfg.logsEndpoint}`);
369
335
  const logs = await probe(cfg.logsEndpoint);
370
336
  if (logs.ok) spin2.stop(`Logs endpoint reachable (HTTP ${logs.status})`);
371
337
  else spin2.fail(`Logs endpoint unreachable: ${logs.error}`);
372
338
  checks.push({ name: 'logs', ...logs });
373
339
 
374
- // SecureNow API (only if API key or logged-in token)
375
340
  const token = config.getToken();
376
341
  if (cfg.apiKey || token) {
377
342
  const spin3 = ui.spinner(`Probing SecureNow API ${cfg.apiUrl}`);
@@ -381,16 +346,15 @@ async function doctor(_args, flags) {
381
346
  checks.push({ name: 'api', ...api });
382
347
  }
383
348
 
384
- // Config sanity
385
349
  const warnings = [];
386
- if (!process.env.SECURENOW_APPID && !process.env.OTEL_SERVICE_NAME) {
387
- warnings.push('No SECURENOW_APPID or OTEL_SERVICE_NAME set a UUID-suffixed name will be generated.');
350
+ if (!cfg.appKey) {
351
+ warnings.push('No app key resolved. Run `npx securenow login` or set app.key in .securenow/credentials.json.');
388
352
  }
389
353
  if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
390
- warnings.push('Using the free-trial collector set SECURENOW_INSTANCE for production.');
354
+ warnings.push('Using the free-trial collector. For production, set app.instance in .securenow/credentials.json.');
391
355
  }
392
- if (!cfg.apiKey && cfg.firewallEnabled) {
393
- warnings.push('Firewall enabled but SECURENOW_API_KEY missing blocklist will not sync.');
356
+ if (!cfg.apiKey) {
357
+ warnings.push('No snk_live firewall API key is resolvable. Run `npx securenow login` or `npx securenow api-key set`.');
394
358
  }
395
359
 
396
360
  const ok = checks.every((c) => c.ok);
package/cli/firewall.js CHANGED
@@ -1,14 +1,19 @@
1
1
  'use strict';
2
2
 
3
- const { api, requireAuth } = require('./client');
4
- const ui = require('./ui');
3
+ const { api, requireAuth } = require('./client');
4
+ const ui = require('./ui');
5
+
6
+ function resolveEnvironment(flags) {
7
+ return flags.env || flags.environment || 'production';
8
+ }
5
9
 
6
10
  async function status(args, flags) {
7
11
  requireAuth();
8
12
  const s = ui.spinner('Checking firewall status');
9
13
 
10
14
  try {
11
- const data = await api.get('/firewall/status');
15
+ const environment = resolveEnvironment(flags);
16
+ const data = await api.get('/firewall/status', { query: { environment } });
12
17
 
13
18
  s.stop('Firewall status retrieved');
14
19
 
@@ -21,7 +26,8 @@ async function status(args, flags) {
21
26
  console.log(` ${ui.c.bold(ui.c.green('Firewall: ENABLED'))}`);
22
27
  console.log('');
23
28
  ui.keyValue([
24
- ['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
29
+ ['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
30
+ ['Environment', data.environment || environment],
25
31
  ['Last updated', data.updatedAt || 'unknown'],
26
32
  ['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
27
33
  ['Allowlist updated', data.allowlistUpdatedAt || 'never'],
@@ -59,7 +65,8 @@ async function testIp(args, flags) {
59
65
  const s = ui.spinner(`Testing IP ${ip}`);
60
66
 
61
67
  try {
62
- const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`);
68
+ const environment = resolveEnvironment(flags);
69
+ const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`, { query: { environment } });
63
70
 
64
71
  s.stop(`IP ${ip} checked`);
65
72
 
@@ -115,10 +122,11 @@ async function setEnabled(args, flags, enabled) {
115
122
  process.exit(1);
116
123
  }
117
124
 
118
- const verb = enabled ? 'Enabling' : 'Disabling';
119
- const s = ui.spinner(`${verb} firewall for ${appKey}`);
120
- try {
121
- const data = await api.patch(`/firewall/app/${encodeURIComponent(appKey)}`, { enabled });
125
+ const environment = resolveEnvironment(flags);
126
+ const verb = enabled ? 'Enabling' : 'Disabling';
127
+ const s = ui.spinner(`${verb} firewall for ${appKey} (${environment})`);
128
+ try {
129
+ const data = await api.patch(`/firewall/app/${encodeURIComponent(appKey)}`, { enabled, environment });
122
130
  s.stop(`Firewall ${enabled ? 'enabled' : 'disabled'}`);
123
131
 
124
132
  if (flags.json) { ui.json(data); return; }
@@ -126,7 +134,8 @@ async function setEnabled(args, flags, enabled) {
126
134
  const app = data.app || {};
127
135
  console.log('');
128
136
  console.log(` ${ui.c.bold(enabled ? ui.c.green('Firewall: ENABLED') : ui.c.yellow('Firewall: DISABLED'))} — ${app.name || appKey}`);
129
- console.log(` ${ui.c.dim('App key:')} ${app.key || appKey}`);
137
+ console.log(` ${ui.c.dim('App key:')} ${app.key || appKey}`);
138
+ console.log(` ${ui.c.dim('Environment:')} ${app.environment || environment}`);
130
139
  console.log('');
131
140
  console.log(` ${ui.c.dim('Running SDK instances pick up the change within ~10s.')}`);
132
141
  console.log('');
@@ -157,10 +166,16 @@ async function appsList(args, flags) {
157
166
  }
158
167
 
159
168
  console.log('');
160
- for (const a of apps) {
161
- const tag = a.firewallEnabled ? ui.c.green('ON ') : ui.c.yellow('OFF');
162
- console.log(` ${tag} ${ui.c.bold(a.name)} ${ui.c.dim(a.key)}`);
163
- }
169
+ for (const a of apps) {
170
+ const tag = a.firewallEnabled ? ui.c.green('ON ') : ui.c.yellow('OFF');
171
+ console.log(` ${tag} ${ui.c.bold(a.name)} ${ui.c.dim(a.key)}`);
172
+ if (a.environments) {
173
+ const envLine = Object.entries(a.environments)
174
+ .map(([env, state]) => `${env}:${state.firewallEnabled ? 'on' : 'off'}`)
175
+ .join(' ');
176
+ if (envLine) console.log(` ${ui.c.dim(envLine)}`);
177
+ }
178
+ }
164
179
  console.log('');
165
180
  console.log(` ${ui.c.dim('Toggle:')} securenow firewall enable --app <key>`);
166
181
  console.log(` ${ui.c.dim(' :')} securenow firewall disable --app <key>`);