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/init.js CHANGED
@@ -1,244 +1,246 @@
1
- 'use strict';
2
-
1
+ 'use strict';
2
+
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const ui = require('./ui');
6
6
  const config = require('./config');
7
7
 
8
- const DEFAULT_ENV = {
9
- SECURENOW_LOGGING_ENABLED: '1',
10
- SECURENOW_CAPTURE_BODY: '1',
11
- SECURENOW_CAPTURE_MULTIPART: '1',
12
- SECURENOW_FIREWALL_ENABLED: '1',
13
- };
14
-
15
- const INSTRUMENTATION_JS = `import { createRequire } from 'node:module';
8
+ const INSTRUMENTATION = `import { createRequire } from 'node:module';
16
9
 
17
10
  const require = createRequire(import.meta.url);
18
11
 
19
12
  export async function register() {
20
13
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;
21
14
 
22
- process.env.SECURENOW_LOGGING_ENABLED ??= '1';
23
- process.env.SECURENOW_CAPTURE_BODY ??= '1';
24
- process.env.SECURENOW_CAPTURE_MULTIPART ??= '1';
25
- process.env.SECURENOW_FIREWALL_ENABLED ??= '1';
26
-
27
15
  const { registerSecureNow } = require('securenow/nextjs');
28
16
  registerSecureNow({ captureBody: true });
29
17
  require('securenow/nextjs-auto-capture');
30
18
  }
31
19
  `;
32
20
 
33
- const INSTRUMENTATION_TS = `import { createRequire } from 'node:module';
21
+ function detectProject(dir) {
22
+ const pkgPath = path.join(dir, 'package.json');
23
+ if (!fs.existsSync(pkgPath)) return { framework: 'unknown' };
34
24
 
35
- const require = createRequire(import.meta.url);
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8').replace(/^\uFEFF/, ''));
26
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
36
27
 
37
- export async function register() {
38
- if (process.env.NEXT_RUNTIME !== 'nodejs') return;
28
+ if (allDeps.next) return { framework: 'nextjs', pkg, pkgPath, nextVersion: allDeps.next };
29
+ if (allDeps.nuxt) return { framework: 'nuxt', pkg, pkgPath };
30
+ if (allDeps.express) return { framework: 'express', pkg, pkgPath };
31
+ if (allDeps.fastify) return { framework: 'fastify', pkg, pkgPath };
32
+ if (allDeps.koa) return { framework: 'koa', pkg, pkgPath };
33
+ if (allDeps.hapi || allDeps['@hapi/hapi']) return { framework: 'hapi', pkg, pkgPath };
34
+ return { framework: 'node', pkg, pkgPath };
35
+ }
39
36
 
40
- process.env.SECURENOW_LOGGING_ENABLED ??= '1';
41
- process.env.SECURENOW_CAPTURE_BODY ??= '1';
42
- process.env.SECURENOW_CAPTURE_MULTIPART ??= '1';
43
- process.env.SECURENOW_FIREWALL_ENABLED ??= '1';
37
+ function findInstrumentationFile(dir) {
38
+ for (const name of ['instrumentation.ts', 'instrumentation.js', 'src/instrumentation.ts', 'src/instrumentation.js']) {
39
+ const p = path.join(dir, name);
40
+ if (fs.existsSync(p)) return p;
41
+ }
42
+ return null;
43
+ }
44
44
 
45
- const { registerSecureNow } = require('securenow/nextjs');
46
- registerSecureNow({ captureBody: true });
47
- require('securenow/nextjs-auto-capture');
45
+ function findNextConfig(dir) {
46
+ for (const name of ['next.config.js', 'next.config.mjs', 'next.config.ts']) {
47
+ const p = path.join(dir, name);
48
+ if (fs.existsSync(p)) return p;
49
+ }
50
+ return null;
48
51
  }
49
- `;
50
-
51
- function detectProject(dir) {
52
- const pkgPath = path.join(dir, 'package.json');
53
- if (!fs.existsSync(pkgPath)) return { framework: 'unknown' };
54
-
55
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
56
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
57
-
58
- if (allDeps.next) return { framework: 'nextjs', pkg, pkgPath, nextVersion: allDeps.next };
59
- if (allDeps.nuxt) return { framework: 'nuxt', pkg, pkgPath };
60
- if (allDeps.express) return { framework: 'express', pkg, pkgPath };
61
- if (allDeps.fastify) return { framework: 'fastify', pkg, pkgPath };
62
- if (allDeps.koa) return { framework: 'koa', pkg, pkgPath };
63
- if (allDeps.hapi || allDeps['@hapi/hapi']) return { framework: 'hapi', pkg, pkgPath };
64
- return { framework: 'node', pkg, pkgPath };
65
- }
66
-
67
- function findInstrumentationFile(dir) {
68
- for (const name of ['instrumentation.ts', 'instrumentation.js', 'src/instrumentation.ts', 'src/instrumentation.js']) {
69
- const p = path.join(dir, name);
70
- if (fs.existsSync(p)) return p;
71
- }
72
- return null;
73
- }
74
-
75
- function findNextConfig(dir) {
76
- for (const name of ['next.config.js', 'next.config.mjs', 'next.config.ts']) {
77
- const p = path.join(dir, name);
78
- if (fs.existsSync(p)) return p;
79
- }
80
- return null;
81
- }
82
-
83
- function hasTypeScript(dir) {
84
- return fs.existsSync(path.join(dir, 'tsconfig.json'));
85
- }
86
-
87
- async function init(_args, flags) {
88
- const dir = process.cwd();
89
- const project = detectProject(dir);
90
-
91
- ui.header('SecureNow Project Setup');
92
-
93
- if (project.framework === 'unknown') {
94
- ui.error('No package.json found. Run this command in your project root.');
95
- process.exit(1);
96
- }
97
-
98
- ui.info(`Detected framework: ${ui.bold(project.framework)}`);
99
-
100
- if (project.framework === 'nextjs') {
101
- await initNextJs(dir, project, flags);
102
- } else if (project.framework === 'nuxt') {
103
- initNuxt(dir, project);
104
- } else {
105
- initNode(dir, project);
106
- }
107
-
108
- initEnv(dir, flags);
109
-
110
- console.log('');
111
- ui.success('Setup complete! Run your app to verify.');
112
- }
113
-
114
- async function initNextJs(dir, project, flags) {
115
- const useTs = hasTypeScript(dir);
116
- const ext = useTs ? 'ts' : 'js';
117
-
118
- const existing = findInstrumentationFile(dir);
119
- if (existing) {
120
- ui.info(`instrumentation file already exists: ${path.relative(dir, existing)}`);
121
- } else {
122
- const filePath = path.join(dir, `instrumentation.${ext}`);
123
- const content = useTs ? INSTRUMENTATION_TS : INSTRUMENTATION_JS;
124
- fs.writeFileSync(filePath, content, 'utf8');
125
- ui.success(`Created instrumentation.${ext}`);
126
- }
127
-
128
- const configPath = findNextConfig(dir);
129
- if (configPath) {
130
- const content = fs.readFileSync(configPath, 'utf8');
131
- if (content.includes('withSecureNow')) {
132
- ui.info('next.config already uses withSecureNow — skipping');
133
- } else if (content.includes('serverExternalPackages') && content.includes('securenow')) {
134
- ui.info('next.config already externalizes securenow — skipping');
135
- } else if (content.includes('serverComponentsExternalPackages') && content.includes('securenow')) {
136
- ui.info('next.config already externalizes securenow — skipping');
137
- } else {
138
- ui.warn(`Update your ${path.basename(configPath)} to use withSecureNow():`);
139
- console.log('');
140
- console.log(` const { withSecureNow } = require('securenow/nextjs-webpack-config');`);
141
- console.log(` module.exports = withSecureNow({ /* your config */ });`);
142
- console.log('');
143
- }
144
- } else {
145
- const newConfigPath = path.join(dir, 'next.config.js');
146
- fs.writeFileSync(newConfigPath, `const { withSecureNow } = require('securenow/nextjs-webpack-config');\n\nmodule.exports = withSecureNow({\n reactStrictMode: true,\n});\n`, 'utf8');
147
- ui.success('Created next.config.js with withSecureNow()');
148
- }
149
- }
150
-
151
- function initNuxt(dir, project) {
152
- const configPath = path.join(dir, 'nuxt.config.ts');
153
- if (fs.existsSync(configPath)) {
154
- const content = fs.readFileSync(configPath, 'utf8');
155
- if (content.includes('securenow/nuxt')) {
156
- ui.info('nuxt.config already references securenow/nuxt — skipping');
157
- } else {
158
- ui.warn('Add securenow/nuxt to your nuxt.config modules:');
159
- console.log('');
160
- console.log(" modules: ['securenow/nuxt'],");
161
- console.log('');
162
- }
163
- } else {
164
- ui.warn('Add securenow/nuxt to your nuxt.config modules array.');
165
- }
166
- }
167
-
168
- function initNode(dir, project) {
169
- const pkg = project.pkg;
170
- const scripts = pkg.scripts || {};
171
-
172
- const startScript = scripts.start || '';
173
- if (startScript.includes('securenow/register')) {
174
- ui.info('start script already uses securenow/register — skipping');
175
- return;
176
- }
177
-
178
- if (startScript) {
179
- ui.warn('Update your start script to include the securenow preload:');
180
- console.log('');
181
- if (startScript.includes('node ')) {
182
- const updated = startScript.replace('node ', 'node -r securenow/register ');
183
- console.log(` "start": "${updated}"`);
184
- } else {
185
- console.log(` "start": "node -r securenow/register ${startScript.replace(/^node\s+/, '')}"`);
186
- }
187
- console.log('');
188
- } else {
189
- ui.warn('Add a start script with the securenow preload:');
190
- console.log('');
191
- console.log(' "start": "node -r securenow/register src/index.js"');
192
- console.log('');
193
- }
194
- }
195
-
196
- function initEnv(dir, flags) {
197
- const envFiles = ['.env', '.env.local'];
198
- let envPath = null;
199
-
200
- for (const f of envFiles) {
201
- const p = path.join(dir, f);
202
- if (fs.existsSync(p)) {
203
- envPath = p;
204
- break;
205
- }
52
+
53
+ function hasTypeScript(dir) {
54
+ return fs.existsSync(path.join(dir, 'tsconfig.json'));
55
+ }
56
+
57
+ function nextMajor(project) {
58
+ const raw = String(project.nextVersion || '').replace(/^[^\d]*/, '');
59
+ const major = parseInt(raw, 10);
60
+ return Number.isFinite(major) ? major : 15;
61
+ }
62
+
63
+ async function init(_args, flags) {
64
+ const dir = process.cwd();
65
+ const project = detectProject(dir);
66
+
67
+ ui.header('SecureNow Project Setup');
68
+
69
+ if (project.framework === 'unknown') {
70
+ ui.error('No package.json found. Run this command in your project root.');
71
+ process.exit(1);
206
72
  }
207
73
 
208
- if (!envPath) envPath = path.join(dir, '.env.local');
74
+ ui.info(`Detected framework: ${ui.bold(project.framework)}`);
75
+ initCredentials(flags);
209
76
 
210
- const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
211
- const additions = [];
212
- for (const [key, value] of Object.entries(DEFAULT_ENV)) {
213
- if (!hasEnvKey(existing, key)) additions.push(`${key}=${value}`);
77
+ if (project.framework === 'nextjs') {
78
+ await initNextJs(dir, project, flags);
79
+ } else if (project.framework === 'nuxt') {
80
+ initNuxt(dir);
81
+ } else {
82
+ initNode(project);
214
83
  }
215
84
 
85
+ console.log('');
86
+ ui.success('Setup complete.');
87
+ ui.info('Run `npx securenow login` if this project is not linked to an app yet.');
88
+ ui.info('Then verify with `npx securenow test-span` and `npx securenow status`.');
89
+ }
90
+
91
+ function initCredentials(flags) {
92
+ config.ensureCredentialDefaults({ local: true });
93
+ config.ensureLocalGitignore();
94
+ const creds = config.loadCredentials();
95
+ creds.config = creds.config || {};
96
+ creds.config.runtime = creds.config.runtime || {};
97
+ creds.config.runtime.deploymentEnvironment = flags.env || flags.environment || 'local';
98
+ config.saveCredentials(creds, { local: true });
99
+
216
100
  const explicitApiKey = flags.key || flags['api-key'] || '';
217
- const hasApiKey = hasEnvKey(existing, 'SECURENOW_API_KEY');
218
- if (explicitApiKey && !hasApiKey) additions.push(`SECURENOW_API_KEY=${explicitApiKey}`);
101
+ if (explicitApiKey) {
102
+ if (!String(explicitApiKey).startsWith('snk_live_')) {
103
+ ui.error('--key must start with snk_live_');
104
+ process.exit(1);
105
+ }
106
+ config.setApiKey(explicitApiKey, { local: true });
107
+ ui.success('Stored firewall API key in .securenow/credentials.json');
108
+ }
109
+
110
+ ui.success('Ensured .securenow/credentials.json has secure defaults and explanations');
111
+ }
112
+
113
+ async function initNextJs(dir, project, flags) {
114
+ const useTs = flags.javascript ? false : flags.typescript ? true : hasTypeScript(dir);
115
+ const ext = useTs ? 'ts' : 'js';
219
116
 
220
- if (additions.length > 0) {
221
- const sep = existing && !existing.endsWith('\n') ? '\n' : '';
222
- fs.appendFileSync(envPath, `${sep}${additions.join('\n')}\n`, 'utf8');
223
- ui.success(`Updated ${path.basename(envPath)} with SecureNow defaults`);
117
+ const existing = findInstrumentationFile(dir);
118
+ if (existing) {
119
+ ui.info(`instrumentation file already exists: ${path.relative(dir, existing)}`);
120
+ printAgentPrompt('instrumentation', path.basename(existing), nextMajor(project));
224
121
  } else {
225
- ui.info(`${path.basename(envPath)} already contains SecureNow defaults`);
122
+ const filePath = path.join(dir, `instrumentation.${ext}`);
123
+ fs.writeFileSync(filePath, INSTRUMENTATION, 'utf8');
124
+ ui.success(`Created instrumentation.${ext}`);
226
125
  }
227
126
 
228
- if (hasApiKey || explicitApiKey) {
229
- ui.info(`SECURENOW_API_KEY is set in ${path.basename(envPath)}`);
230
- } else if (config.getApiKey()) {
231
- ui.info('Using firewall API key from project .securenow/credentials.json');
127
+ const configPath = findNextConfig(dir);
128
+ if (configPath) {
129
+ const patched = patchNextConfig(configPath, nextMajor(project));
130
+ if (patched === 'already') {
131
+ ui.info(`${path.basename(configPath)} already externalizes securenow`);
132
+ } else if (patched === 'patched') {
133
+ ui.success(`Updated ${path.basename(configPath)} with SecureNow server externalization`);
134
+ } else {
135
+ ui.warn(`Could not safely edit ${path.basename(configPath)} automatically.`);
136
+ printAgentPrompt('next-config', path.basename(configPath), nextMajor(project));
137
+ }
138
+ } else {
139
+ const newConfigPath = path.join(dir, 'next.config.mjs');
140
+ fs.writeFileSync(newConfigPath, `/** @type {import('next').NextConfig} */
141
+ const nextConfig = {
142
+ serverExternalPackages: ['securenow'],
143
+ };
144
+
145
+ export default nextConfig;
146
+ `, 'utf8');
147
+ ui.success('Created next.config.mjs');
148
+ }
149
+ }
150
+
151
+ function patchNextConfig(configPath, major) {
152
+ const content = fs.readFileSync(configPath, 'utf8');
153
+ const serverExternalWithSecureNow = /serverExternalPackages\s*:\s*\[[\s\S]*?['"]securenow['"][\s\S]*?\]/m.test(content);
154
+ const serverComponentsWithSecureNow = /serverComponentsExternalPackages\s*:\s*\[[\s\S]*?['"]securenow['"][\s\S]*?\]/m.test(content);
155
+ if (serverExternalWithSecureNow || serverComponentsWithSecureNow || content.includes('withSecureNow(')) {
156
+ return 'already';
157
+ }
158
+
159
+ if (major < 15) return 'manual';
160
+
161
+ const existingServerExternal = content.match(/serverExternalPackages\s*:\s*\[([\s\S]*?)\]/m);
162
+ if (existingServerExternal) {
163
+ const current = existingServerExternal[1].trim().replace(/,\s*$/, '');
164
+ const replacement = `serverExternalPackages: [${current ? `${current}, ` : ''}'securenow']`;
165
+ fs.writeFileSync(configPath, content.replace(existingServerExternal[0], replacement), 'utf8');
166
+ return 'patched';
167
+ }
168
+
169
+ const insert = ` serverExternalPackages: ['securenow'],\n`;
170
+ const patterns = [
171
+ /(const\s+nextConfig\s*=\s*{\s*\r?\n)/,
172
+ /(export\s+default\s+{\s*\r?\n)/,
173
+ /(module\.exports\s*=\s*{\s*\r?\n)/,
174
+ ];
175
+
176
+ for (const pattern of patterns) {
177
+ if (pattern.test(content)) {
178
+ fs.writeFileSync(configPath, content.replace(pattern, `$1${insert}`), 'utf8');
179
+ return 'patched';
180
+ }
181
+ }
182
+
183
+ return 'manual';
184
+ }
185
+
186
+ function printAgentPrompt(kind, filename, major) {
187
+ console.log('');
188
+ ui.heading('Codex/Claude prompt');
189
+ console.log([
190
+ 'Set up SecureNow in this existing Next.js project without using .env files.',
191
+ 'Use .securenow/credentials.json for local and production configuration; do not add .env files.',
192
+ kind === 'instrumentation'
193
+ ? `Merge this into ${filename}: in register(), return unless process.env.NEXT_RUNTIME === "nodejs"; use createRequire(import.meta.url); require("securenow/nextjs").registerSecureNow({ captureBody: true }); then require("securenow/nextjs-auto-capture"). Preserve all existing instrumentation.`
194
+ : null,
195
+ kind === 'next-config' && major >= 15
196
+ ? `Update ${filename} while preserving existing config: add securenow to serverExternalPackages, e.g. serverExternalPackages: [...(existing || []), "securenow"].`
197
+ : null,
198
+ kind === 'next-config' && major < 15
199
+ ? `Update ${filename} while preserving existing config: enable experimental.instrumentationHook and add securenow to experimental.serverComponentsExternalPackages.`
200
+ : null,
201
+ 'Verify with: npx securenow env, npx securenow test-span, npx securenow status, and the project build command.',
202
+ ].filter(Boolean).join('\n'));
203
+ console.log('');
204
+ }
205
+
206
+ function initNuxt(dir) {
207
+ const configPath = path.join(dir, 'nuxt.config.ts');
208
+ if (fs.existsSync(configPath)) {
209
+ const content = fs.readFileSync(configPath, 'utf8');
210
+ if (content.includes('securenow/nuxt')) {
211
+ ui.info('nuxt.config already references securenow/nuxt');
212
+ } else {
213
+ ui.warn('Add securenow/nuxt to your nuxt.config modules array.');
214
+ }
232
215
  } else {
233
- ui.warn(`Add your API key to ${path.basename(envPath)}:`);
216
+ ui.warn('Add securenow/nuxt to your nuxt.config modules array.');
217
+ }
218
+ }
219
+
220
+ function initNode(project) {
221
+ const scripts = project.pkg.scripts || {};
222
+ const startScript = scripts.start || '';
223
+ if (startScript.includes('securenow/register')) {
224
+ ui.info('start script already uses securenow/register');
225
+ return;
226
+ }
227
+
228
+ if (startScript) {
229
+ ui.warn('Update your start script to include the securenow preload:');
234
230
  console.log('');
235
- console.log(' SECURENOW_API_KEY=snk_live_...');
231
+ if (startScript.includes('node ')) {
232
+ const updated = startScript.replace('node ', 'node -r securenow/register ');
233
+ console.log(` "start": "${updated}"`);
234
+ } else {
235
+ console.log(` "start": "node -r securenow/register ${startScript.replace(/^node\s+/, '')}"`);
236
+ }
237
+ console.log('');
238
+ } else {
239
+ ui.warn('Add a start script with the securenow preload:');
240
+ console.log('');
241
+ console.log(' "start": "node -r securenow/register src/index.js"');
236
242
  console.log('');
237
243
  }
238
244
  }
239
245
 
240
- function hasEnvKey(content, key) {
241
- return new RegExp(`^\\s*${key}\\s*=`, 'm').test(content || '');
242
- }
243
-
244
- module.exports = { init };
246
+ module.exports = { init };
package/cli/monitor.js CHANGED
@@ -1,12 +1,23 @@
1
1
  'use strict';
2
2
 
3
- const { api, requireAuth } = require('./client');
4
- const config = require('./config');
5
- const ui = require('./ui');
6
-
7
- function resolveApp(flags) {
8
- return flags.app || config.getDefaultApp();
9
- }
3
+ const { api, requireAuth } = require('./client');
4
+ const config = require('./config');
5
+ const ui = require('./ui');
6
+ const appConfig = require('../app-config');
7
+
8
+ function resolveApp(flags) {
9
+ return flags.app || config.getDefaultApp();
10
+ }
11
+
12
+ function resolveEnvironment(flags) {
13
+ return flags.env || flags.environment || null;
14
+ }
15
+
16
+ function addEnvironment(query, flags) {
17
+ const environment = resolveEnvironment(flags);
18
+ if (environment) query.environment = environment;
19
+ return query;
20
+ }
10
21
 
11
22
  // ── Traces ──
12
23
 
@@ -20,10 +31,11 @@ async function tracesList(args, flags) {
20
31
 
21
32
  const s = ui.spinner('Fetching traces');
22
33
  try {
23
- const query = {
24
- appKeys: appKey,
25
- limit: flags.limit || 20,
26
- };
34
+ const query = {
35
+ appKeys: appKey,
36
+ limit: flags.limit || 20,
37
+ };
38
+ addEnvironment(query, flags);
27
39
  if (flags.start) query.from = flags.start;
28
40
  if (flags.end) query.to = flags.end;
29
41
 
@@ -35,7 +47,8 @@ async function tracesList(args, flags) {
35
47
 
36
48
  console.log('');
37
49
  const rows = traces.map(t => [
38
- ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
50
+ ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
51
+ t.environment || 'production',
39
52
  t.operationName || t.name || t.serviceName || '—',
40
53
  ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
41
54
  ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
@@ -44,7 +57,7 @@ async function tracesList(args, flags) {
44
57
  ui.timeAgo(t.timestamp),
45
58
  ]);
46
59
 
47
- ui.table(['Trace ID', 'Operation', 'Status', 'Duration', 'Method', 'URL', 'Time'], rows);
60
+ ui.table(['Trace ID', 'Env', 'Operation', 'Status', 'Duration', 'Method', 'URL', 'Time'], rows);
48
61
  console.log('');
49
62
  } catch (err) {
50
63
  s.fail('Failed to fetch traces');
@@ -62,9 +75,10 @@ async function tracesShow(args, flags) {
62
75
 
63
76
  const s = ui.spinner('Fetching trace details');
64
77
  try {
65
- const appKey = resolveApp(flags);
66
- const traceQuery = appKey ? { appKeys: appKey } : {};
67
- const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
78
+ const appKey = resolveApp(flags);
79
+ const traceQuery = appKey ? { appKeys: appKey } : {};
80
+ addEnvironment(traceQuery, flags);
81
+ const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
68
82
  s.stop('Trace loaded');
69
83
 
70
84
  if (flags.json) { ui.json(data); return; }
@@ -103,9 +117,29 @@ async function tracesAnalyze(args, flags) {
103
117
  process.exit(1);
104
118
  }
105
119
 
106
- const s = ui.spinner('Analyzing trace with AI');
107
- try {
108
- let result = await api.post('/traces/analyze', { traceId });
120
+ const s = ui.spinner('Analyzing trace with AI');
121
+ try {
122
+ const environment = resolveEnvironment(flags);
123
+ const appKey = resolveApp(flags);
124
+ if (!appKey) {
125
+ s.fail('Missing app key');
126
+ ui.error('Use --app <key> or set a default with `securenow config set defaultApp <key>`');
127
+ process.exit(1);
128
+ }
129
+
130
+ const query = { appKeys: appKey };
131
+ if (environment) query.environment = environment;
132
+ const [traceData, logsData] = await Promise.all([
133
+ api.get(`/traces/${traceId}`, { query }),
134
+ api.get(`/logs/trace/${traceId}`, { query }).catch(() => ({ logs: [] })),
135
+ ]);
136
+
137
+ const body = {
138
+ traceData: traceData.spans || traceData.traceData || [],
139
+ logs: logsData.logs || [],
140
+ };
141
+ if (environment) body.environment = environment;
142
+ let result = await api.post('/traces/analyze', body);
109
143
 
110
144
  if (result.analysisId && result.status === 'running') {
111
145
  const analysisId = result.analysisId;
@@ -223,10 +257,11 @@ async function logsList(args, flags) {
223
257
  limit: flags.limit || 200,
224
258
  from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
225
259
  to: flags.end || new Date(now).toISOString(),
226
- };
227
- if (severity) query.severity = severity;
228
-
229
- const data = await api.get('/logs', { query });
260
+ };
261
+ if (severity) query.severity = severity;
262
+ addEnvironment(query, flags);
263
+
264
+ const data = await api.get('/logs', { query });
230
265
  const logs = data.logs || [];
231
266
  s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
232
267
 
@@ -244,7 +279,8 @@ async function logsList(args, flags) {
244
279
  const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
245
280
  const body = log.body || log.message || log.severityText || '';
246
281
 
247
- console.log(` ${time} ${levelColor(level.padEnd(7))} ${body}`);
282
+ const env = log.environment ? ` ${ui.c.dim(`[${log.environment}]`)}` : '';
283
+ console.log(` ${time} ${levelColor(level.padEnd(7))}${env} ${body}`);
248
284
 
249
285
  if (log.traceId && flags.verbose) {
250
286
  console.log(` ${ui.c.dim(` trace=${log.traceId} span=${log.spanId || ''}`)}`);
@@ -265,9 +301,13 @@ async function logsTrace(args, flags) {
265
301
  process.exit(1);
266
302
  }
267
303
 
268
- const s = ui.spinner('Fetching logs for trace');
269
- try {
270
- const data = await api.get(`/logs/trace/${traceId}`);
304
+ const s = ui.spinner('Fetching logs for trace');
305
+ try {
306
+ const query = {};
307
+ const appKey = resolveApp(flags);
308
+ if (appKey) query.appKeys = appKey;
309
+ addEnvironment(query, flags);
310
+ const data = await api.get(`/logs/trace/${traceId}`, { query });
271
311
  const logs = data.logs || [];
272
312
  s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
273
313
 
@@ -283,7 +323,8 @@ async function logsTrace(args, flags) {
283
323
 
284
324
  const parsedTime = ui.parseTimestamp(log.timestamp);
285
325
  const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
286
- console.log(` ${time} ${levelColor(level.padEnd(7))} ${log.body || log.message || ''}`);
326
+ const env = log.environment ? ` ${ui.c.dim(`[${log.environment}]`)}` : '';
327
+ console.log(` ${time} ${levelColor(level.padEnd(7))}${env} ${log.body || log.message || ''}`);
287
328
  }
288
329
  console.log('');
289
330
  } catch (err) {
@@ -400,22 +441,45 @@ async function status(args, flags) {
400
441
  ui.table(['Name', 'Key', 'Hosts'], rows);
401
442
  }
402
443
 
403
- const appKey = resolveApp(flags);
404
- if (appKey) {
444
+ const appKey = resolveApp(flags);
445
+ const requestedEnv = resolveEnvironment(flags) || appConfig.resolveDeploymentEnvironment() || 'production';
446
+ const showAllEnvs = requestedEnv === 'all' || requestedEnv === '*';
447
+ {
405
448
  try {
406
- const protectionData = await api.get('/applications/protection-status');
407
- const statuses = protectionData.statuses;
408
- if (statuses && Object.keys(statuses).length > 0) {
409
- ui.subheading('Protection Status');
410
- console.log('');
411
- const rows = Object.entries(statuses).map(([id, s]) => [
412
- ui.c.dim(ui.truncate(id, 12)),
413
- s.protected ? ui.c.green('● protected') : ui.c.red('○ unprotected'),
414
- String(s.traceCount || 0),
415
- s.lastTrace ? ui.timeAgo(s.lastTrace) : ui.c.dim('—'),
416
- ]);
417
- ui.table(['App ID', 'Status', 'Traces (15m)', 'Last Trace'], rows);
418
- }
449
+ const protectionData = await api.get('/applications/protection-status');
450
+ const statuses = protectionData.statuses;
451
+ if (statuses && Object.keys(statuses).length > 0) {
452
+ const appById = new Map(apps.map((app) => [String(app._id || app.id), app]));
453
+ const envRows = [];
454
+
455
+ for (const [id, status] of Object.entries(statuses)) {
456
+ const app = appById.get(String(id));
457
+ if (appKey && app && app.key !== appKey) continue;
458
+
459
+ const environments = status.environments || { production: status.production || status };
460
+ const entries = showAllEnvs
461
+ ? Object.entries(environments)
462
+ : [[requestedEnv, environments[requestedEnv] || status.production || status]];
463
+
464
+ for (const [envName, envStatus] of entries) {
465
+ envRows.push([
466
+ app?.name || ui.c.dim(ui.truncate(id, 12)),
467
+ envName,
468
+ envStatus.firewallEnabled ? ui.c.green('on') : ui.c.dim('off'),
469
+ envStatus.protected ? ui.c.green('live') : ui.c.dim('idle'),
470
+ String(envStatus.traceCount || 0),
471
+ envStatus.lastTrace ? ui.timeAgo(envStatus.lastTrace) : ui.c.dim('-'),
472
+ ]);
473
+ }
474
+ }
475
+
476
+ if (envRows.length) {
477
+ ui.subheading(`Protection Status (${showAllEnvs ? 'all envs' : requestedEnv})`);
478
+ console.log('');
479
+ ui.table(['App', 'Env', 'Firewall', 'Traces', 'Count (15m)', 'Last Trace'], envRows);
480
+ }
481
+
482
+ }
419
483
  } catch {}
420
484
  }
421
485