securenow 7.5.0 → 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 (51) 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 +81 -98
  12. package/cli/firewall.js +29 -14
  13. package/cli/init.js +246 -201
  14. package/cli/monitor.js +107 -43
  15. package/cli/security.js +24 -12
  16. package/cli/ui.js +22 -12
  17. package/cli/utils.js +2 -1
  18. package/cli.js +71 -39
  19. package/console-instrumentation.js +1 -1
  20. package/docs/ENVIRONMENT-VARIABLES.md +137 -863
  21. package/docs/ENVIRONMENTS.md +60 -0
  22. package/docs/EXPRESS-SETUP-GUIDE.md +3 -0
  23. package/docs/FIREWALL-GUIDE.md +3 -0
  24. package/docs/INDEX.md +6 -8
  25. package/docs/LOGGING-GUIDE.md +3 -0
  26. package/docs/MCP-GUIDE.md +8 -0
  27. package/docs/NEXTJS-GUIDE.md +3 -0
  28. package/docs/NEXTJS-QUICKSTART.md +24 -16
  29. package/docs/NUXT-GUIDE.md +3 -0
  30. package/docs/QUICKSTART-BODY-CAPTURE.md +3 -0
  31. package/docs/REQUEST-BODY-CAPTURE.md +3 -0
  32. package/firewall-cloud.js +10 -10
  33. package/firewall-only.js +25 -23
  34. package/firewall.js +47 -29
  35. package/free-trial-banner.js +1 -1
  36. package/mcp/catalog.js +104 -17
  37. package/nextjs-auto-capture.d.ts +7 -4
  38. package/nextjs-auto-capture.js +7 -7
  39. package/nextjs-middleware.js +4 -3
  40. package/nextjs-wrapper.js +6 -6
  41. package/nextjs.d.ts +36 -25
  42. package/nextjs.js +47 -55
  43. package/nuxt-server-plugin.mjs +35 -51
  44. package/nuxt.d.ts +29 -23
  45. package/package.json +1 -1
  46. package/postinstall.js +27 -61
  47. package/register.d.ts +19 -33
  48. package/register.js +8 -8
  49. package/resolve-ip.js +4 -5
  50. package/tracing.d.ts +21 -19
  51. 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,76 +4,61 @@ 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 firewallEnabled =
43
- !!apiKey && process.env.SECURENOW_FIREWALL_ENABLED !== '0';
22
+ const apiKey = firewall.apiKey || '';
23
+ const firewallEnabled = !!apiKey && firewall.enabled;
44
24
 
45
25
  return {
46
26
  serviceName,
47
- instance,
48
- tracesEndpoint,
49
- logsEndpoint,
50
- headers,
27
+ appKey: resolvedApp.appKey || null,
28
+ deploymentEnvironment,
29
+ instance: endpoints.endpointBase,
30
+ tracesEndpoint: endpoints.tracesUrl,
31
+ logsEndpoint: endpoints.logsUrl,
32
+ headers: endpoints.headers,
51
33
  apiKey,
52
- apiUrl,
53
- loggingEnabled,
54
- captureBody,
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),
55
38
  firewallEnabled,
56
39
  firewallLayers: {
57
40
  http: firewallEnabled,
58
- tcp: firewallEnabled && process.env.SECURENOW_FIREWALL_TCP === '1',
59
- iptables: firewallEnabled && process.env.SECURENOW_FIREWALL_IPTABLES === '1',
60
- 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,
61
44
  },
62
45
  };
63
46
  }
64
47
 
65
- function parseHeaders(str) {
66
- const out = {};
67
- if (!str) return out;
68
- for (const pair of str.split(',')) {
69
- const [k, ...rest] = pair.split('=');
70
- if (!k) continue;
71
- out[k.trim()] = rest.join('=').trim();
72
- }
73
- return out;
48
+ function maskSecret(value) {
49
+ if (!value) return '';
50
+ const text = String(value);
51
+ if (text.length <= 12) return '***';
52
+ return `${text.slice(0, 12)}...${text.slice(-4)}`;
74
53
  }
75
54
 
76
- // ── HTTP helpers ──
55
+ function redactConfig(cfg) {
56
+ return {
57
+ ...cfg,
58
+ headers: cfg.headers && Object.keys(cfg.headers).length ? '***' : '',
59
+ apiKey: cfg.apiKey ? maskSecret(cfg.apiKey) : '',
60
+ };
61
+ }
77
62
 
78
63
  function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs = 5000 }) {
79
64
  return new Promise((resolve, reject) => {
@@ -104,8 +89,6 @@ function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs
104
89
  });
105
90
  }
106
91
 
107
- // ── OTLP/HTTP JSON payloads ──
108
-
109
92
  function attr(key, value) {
110
93
  if (typeof value === 'number') {
111
94
  if (Number.isInteger(value)) return { key, value: { intValue: String(value) } };
@@ -118,7 +101,7 @@ function attr(key, value) {
118
101
  function resourceAttrs(cfg, extra = {}) {
119
102
  const base = {
120
103
  'service.name': cfg.serviceName,
121
- 'deployment.environment': process.env.NODE_ENV || 'development',
104
+ 'deployment.environment': cfg.deploymentEnvironment || appConfig.resolveDeploymentEnvironment(),
122
105
  'telemetry.sdk.name': 'securenow-cli',
123
106
  'telemetry.sdk.language': 'nodejs',
124
107
  ...extra,
@@ -184,18 +167,19 @@ function buildLogPayload(cfg, { message, level = 'info', attributes = {} }) {
184
167
  };
185
168
  }
186
169
 
187
- // ── Commands ──
188
-
189
170
  async function testSpan(args, flags) {
190
- const cfg = resolvedConfig();
171
+ const cfg = resolvedConfig(flags);
191
172
  const spanName = args[0] || 'securenow.cli.test-span';
192
173
  const headers = {
193
174
  'Content-Type': 'application/json',
194
- ...parseHeaders(cfg.headers),
175
+ ...cfg.headers,
195
176
  };
196
177
  const payload = buildTracePayload(cfg, {
197
178
  name: spanName,
198
- attributes: { 'test.source': 'securenow-cli' },
179
+ attributes: {
180
+ 'test.source': 'securenow-cli',
181
+ 'securenow.environment': cfg.deploymentEnvironment,
182
+ },
199
183
  });
200
184
 
201
185
  const spin = ui.spinner(`Sending test span to ${cfg.tracesEndpoint}`);
@@ -207,7 +191,7 @@ async function testSpan(args, flags) {
207
191
  });
208
192
  if (res.status >= 200 && res.status < 300) {
209
193
  spin.stop(`Span accepted (HTTP ${res.status})`);
210
- 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 });
211
195
  return;
212
196
  }
213
197
  spin.fail(`Collector returned HTTP ${res.status}`);
@@ -229,9 +213,9 @@ async function logSend(args, flags) {
229
213
  process.exit(1);
230
214
  }
231
215
 
232
- const cfg = resolvedConfig();
216
+ const cfg = resolvedConfig(flags);
233
217
  if (!cfg.loggingEnabled) {
234
- ui.warn('Logging is disabled (SECURENOW_LOGGING_ENABLED=0). Sending anyway.');
218
+ ui.warn('Logging is disabled in SecureNow credentials/config. Sending anyway.');
235
219
  }
236
220
 
237
221
  const level = (flags.level || 'info').toString();
@@ -245,9 +229,16 @@ async function logSend(args, flags) {
245
229
 
246
230
  const headers = {
247
231
  'Content-Type': 'application/json',
248
- ...parseHeaders(cfg.headers),
232
+ ...cfg.headers,
249
233
  };
250
- 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
+ });
251
242
 
252
243
  const spin = ui.spinner(`Sending log to ${cfg.logsEndpoint}`);
253
244
  try {
@@ -258,7 +249,7 @@ async function logSend(args, flags) {
258
249
  });
259
250
  if (res.status >= 200 && res.status < 300) {
260
251
  spin.stop(`Log accepted (HTTP ${res.status})`);
261
- 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 });
262
253
  return;
263
254
  }
264
255
  spin.fail(`Collector returned HTTP ${res.status}`);
@@ -272,12 +263,8 @@ async function logSend(args, flags) {
272
263
  }
273
264
  }
274
265
 
275
- // ── doctor / env ──
276
-
277
266
  async function probe(endpoint, timeoutMs = 3000) {
278
267
  try {
279
- // A HEAD or empty POST is safer than sending real OTLP. Most collectors
280
- // return 400/405 for a malformed request — that still proves reachability.
281
268
  const res = await httpRequest({
282
269
  method: 'POST',
283
270
  endpoint,
@@ -293,43 +280,43 @@ async function probe(endpoint, timeoutMs = 3000) {
293
280
 
294
281
  function env(_args, flags) {
295
282
  const cfg = resolvedConfig();
296
- const vars = {
297
- SECURENOW_APPID: process.env.SECURENOW_APPID || null,
298
- OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME || null,
299
- SECURENOW_INSTANCE: process.env.SECURENOW_INSTANCE || null,
300
- OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || null,
301
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || null,
302
- OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || null,
303
- OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS ? '***' : null,
304
- SECURENOW_API_KEY: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
305
- SECURENOW_API_URL: cfg.apiUrl,
306
- SECURENOW_LOGGING_ENABLED: cfg.loggingEnabled ? '1' : '0',
307
- SECURENOW_CAPTURE_BODY: cfg.captureBody ? '1' : '0',
308
- SECURENOW_NO_UUID: process.env.SECURENOW_NO_UUID || `(auto: ${require('../app-config').resolveNoUuid() ? '1' : '0'})`,
309
- SECURENOW_FIREWALL_TCP: process.env.SECURENOW_FIREWALL_TCP || '0',
310
- SECURENOW_FIREWALL_IPTABLES: process.env.SECURENOW_FIREWALL_IPTABLES || '0',
311
- SECURENOW_FIREWALL_CLOUD: process.env.SECURENOW_FIREWALL_CLOUD || null,
312
- 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,
313
298
  };
314
299
 
315
300
  if (flags.json) {
316
- ui.json({ resolved: cfg, env: vars });
301
+ ui.json({ resolved: redactConfig(cfg), config: resolved });
317
302
  return;
318
303
  }
319
304
 
320
305
  ui.heading('Resolved configuration');
321
306
  ui.keyValue([
322
307
  ['Service name', cfg.serviceName],
308
+ ['Environment', cfg.deploymentEnvironment],
323
309
  ['Traces endpoint', cfg.tracesEndpoint],
324
310
  ['Logs endpoint', cfg.logsEndpoint],
325
311
  ['Logging', cfg.loggingEnabled ? ui.c.green('enabled') : ui.c.dim('disabled')],
326
312
  ['Body capture', cfg.captureBody ? ui.c.green('enabled') : ui.c.dim('disabled')],
313
+ ['Multipart capture', cfg.captureMultipart ? ui.c.green('enabled') : ui.c.dim('disabled')],
327
314
  ['Firewall', cfg.firewallEnabled ? ui.c.green('enabled') : ui.c.dim('disabled (no API key)')],
328
315
  ]);
329
316
 
330
- ui.heading('Environment variables');
317
+ ui.heading('Resolved credentials');
331
318
  ui.keyValue(
332
- 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)])
333
320
  );
334
321
  console.log('');
335
322
  }
@@ -338,21 +325,18 @@ async function doctor(_args, flags) {
338
325
  const cfg = resolvedConfig();
339
326
  const checks = [];
340
327
 
341
- // Collector traces
342
328
  const spin1 = ui.spinner(`Probing traces endpoint ${cfg.tracesEndpoint}`);
343
329
  const traces = await probe(cfg.tracesEndpoint);
344
330
  if (traces.ok) spin1.stop(`Traces endpoint reachable (HTTP ${traces.status})`);
345
331
  else spin1.fail(`Traces endpoint unreachable: ${traces.error}`);
346
332
  checks.push({ name: 'traces', ...traces });
347
333
 
348
- // Collector logs
349
334
  const spin2 = ui.spinner(`Probing logs endpoint ${cfg.logsEndpoint}`);
350
335
  const logs = await probe(cfg.logsEndpoint);
351
336
  if (logs.ok) spin2.stop(`Logs endpoint reachable (HTTP ${logs.status})`);
352
337
  else spin2.fail(`Logs endpoint unreachable: ${logs.error}`);
353
338
  checks.push({ name: 'logs', ...logs });
354
339
 
355
- // SecureNow API (only if API key or logged-in token)
356
340
  const token = config.getToken();
357
341
  if (cfg.apiKey || token) {
358
342
  const spin3 = ui.spinner(`Probing SecureNow API ${cfg.apiUrl}`);
@@ -362,22 +346,21 @@ async function doctor(_args, flags) {
362
346
  checks.push({ name: 'api', ...api });
363
347
  }
364
348
 
365
- // Config sanity
366
349
  const warnings = [];
367
- if (!process.env.SECURENOW_APPID && !process.env.OTEL_SERVICE_NAME) {
368
- 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.');
369
352
  }
370
353
  if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
371
- 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.');
372
355
  }
373
- if (!cfg.apiKey && cfg.firewallEnabled) {
374
- 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`.');
375
358
  }
376
359
 
377
360
  const ok = checks.every((c) => c.ok);
378
361
 
379
362
  if (flags.json) {
380
- ui.json({ ok, resolved: cfg, checks, warnings });
363
+ ui.json({ ok, resolved: redactConfig(cfg), checks, warnings });
381
364
  process.exit(ok ? 0 : 1);
382
365
  }
383
366
 
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>`);