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/app-config.js CHANGED
@@ -1,35 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Shared app-configuration resolver.
4
+ * Shared SecureNow configuration resolver.
5
5
  *
6
- * Used by both the SDK (tracing.js, nextjs.js, nuxt-server-plugin.mjs) and
7
- * the CLI to answer three questions with a single source of truth:
8
- *
9
- * - What is the app routing key? (resolveAppKey) — used as OTel service.name
10
- * AND as x-api-key header. The collector
11
- * filters ClickHouse queries by
12
- * `resource_string_service$$name IN (...)`
13
- * so service.name MUST be the app.key UUID.
14
- * - What human label to show? (resolveAppName) — display only, never
15
- * sent to the collector.
16
- * - Which OTLP collector to hit? (resolveInstance)
17
- * - Which firewall API key? (resolveApiKey) — the snk_live_... key the
18
- * firewall sends as Bearer to /api/firewall
19
- * for blocklist sync. Separate from the
20
- * app routing key: this one is user-scoped.
21
- *
22
- * Resolution order (first non-empty wins):
23
- *
24
- * 1. Explicit environment variable
25
- * (SECURENOW_APPID / SECURENOW_API_KEY / SECURENOW_INSTANCE)
26
- * 2. Project-local credentials (./.securenow/credentials.json)
27
- * 3. Global credentials (~/.securenow/credentials.json)
28
- * 4. package.json#name (for the human label only — can't route on this)
29
- * 5. Hard default / null
30
- *
31
- * Credentials file schema:
32
- * { token, email, expiresAt, apiKey: <snk_live_...>, app: { key, name, instance } }
6
+ * Local development and production are driven by ./.securenow/credentials.json.
7
+ * Legacy environment variables are only fallback inputs for existing installs;
8
+ * every SDK setting has a file-backed equivalent so customers do not need .env
9
+ * files.
33
10
  */
34
11
 
35
12
  const fs = require('fs');
@@ -37,6 +14,147 @@ const path = require('path');
37
14
  const os = require('os');
38
15
 
39
16
  const FREE_TRIAL_INSTANCE = 'https://freetrial.securenow.ai:4318';
17
+ const DEFAULT_API_URL = 'https://api.securenow.ai';
18
+ const CONFIG_SCHEMA_VERSION = 2;
19
+
20
+ const DEFAULT_CONFIG = Object.freeze({
21
+ logging: {
22
+ enabled: true,
23
+ },
24
+ capture: {
25
+ body: true,
26
+ multipart: true,
27
+ maxBodySize: 10240,
28
+ sensitiveFields: [],
29
+ },
30
+ otel: {
31
+ endpoint: null,
32
+ tracesEndpoint: null,
33
+ logsEndpoint: null,
34
+ headers: {},
35
+ logLevel: 'none',
36
+ disableInstrumentations: [],
37
+ },
38
+ runtime: {
39
+ deploymentEnvironment: 'production',
40
+ noUuid: null,
41
+ strict: false,
42
+ testSpan: false,
43
+ hideBanner: false,
44
+ },
45
+ firewall: {
46
+ enabled: true,
47
+ apiUrl: DEFAULT_API_URL,
48
+ versionCheckInterval: 10,
49
+ syncInterval: 300,
50
+ failMode: 'open',
51
+ statusCode: 403,
52
+ log: true,
53
+ tcp: false,
54
+ iptables: false,
55
+ cloud: null,
56
+ cloudDryRun: false,
57
+ cloudflare: {
58
+ apiToken: null,
59
+ accountId: null,
60
+ },
61
+ aws: {
62
+ wafIpSetId: null,
63
+ wafIpSetName: 'securenow-blocklist',
64
+ wafScope: 'REGIONAL',
65
+ },
66
+ gcp: {
67
+ projectId: null,
68
+ securityPolicy: null,
69
+ },
70
+ },
71
+ networking: {
72
+ trustedProxies: [],
73
+ },
74
+ });
75
+
76
+ const CONFIG_EXPLANATIONS = Object.freeze({
77
+ 'token': 'CLI session token written by `npx securenow login`. Secret: do not commit.',
78
+ 'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by login or `securenow api-key set`. Secret: do not commit.',
79
+ 'app.key': 'SecureNow application routing UUID. The SDK uses this as OTel service.name so dashboard queries match exactly.',
80
+ 'app.name': 'Human-readable app label shown in CLI output.',
81
+ 'app.instance': 'OTLP collector base URL for this app. Production should provide this same credentials file at .securenow/credentials.json.',
82
+ 'config.logging.enabled': 'Secure default: console log forwarding is on. Set false to disable OTLP logs.',
83
+ 'config.capture.body': 'Secure default: JSON, GraphQL, and form request bodies are captured with redaction.',
84
+ 'config.capture.multipart': 'Secure default: multipart text fields and file metadata are captured. File content is never buffered.',
85
+ 'config.capture.maxBodySize': 'Maximum body bytes captured per request. Default 10KB limits memory and sensitive data exposure.',
86
+ 'config.capture.sensitiveFields': 'Extra field-name fragments to redact in addition to SecureNow built-ins.',
87
+ 'config.otel.endpoint': 'Optional OTLP base endpoint override. Usually app.instance is enough.',
88
+ 'config.otel.tracesEndpoint': 'Optional full traces endpoint override, for split collectors.',
89
+ 'config.otel.logsEndpoint': 'Optional full logs endpoint override, for split collectors.',
90
+ 'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-api-key from app.key when missing.',
91
+ 'config.otel.logLevel': 'OpenTelemetry diagnostic log level: none, error, warn, info, or debug.',
92
+ 'config.otel.disableInstrumentations': 'Optional OTel instrumentation package names to disable.',
93
+ 'config.runtime.deploymentEnvironment': 'deployment.environment resource attribute. Set this in the credentials file for production.',
94
+ 'config.runtime.noUuid': 'null means auto: true when app.key is present. Set true/false only for advanced routing needs.',
95
+ 'config.runtime.strict': 'If true, PM2/cluster workers exit when no app identity is resolvable.',
96
+ 'config.runtime.testSpan': 'If true, emit a startup smoke span. Prefer `npx securenow test-span` for manual checks.',
97
+ 'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
98
+ 'config.firewall.enabled': 'Secure default: app firewall enforcement starts when apiKey is present and the dashboard toggle is on.',
99
+ 'config.firewall.apiUrl': 'SecureNow API base URL for firewall sync.',
100
+ 'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version checks.',
101
+ 'config.firewall.syncInterval': 'Seconds between full firewall blocklist syncs.',
102
+ 'config.firewall.failMode': 'open allows traffic if SecureNow is temporarily unreachable; closed blocks all on sync failure.',
103
+ 'config.firewall.statusCode': 'HTTP status returned by application-layer firewall blocks.',
104
+ 'config.firewall.log': 'Log firewall decisions locally.',
105
+ 'config.firewall.tcp': 'Layer 2 TCP drop. Default false because it patches sockets.',
106
+ 'config.firewall.iptables': 'Layer 3 OS firewall. Default false because it requires Linux root/CAP_NET_ADMIN.',
107
+ 'config.firewall.cloud': 'Layer 4 WAF provider: cloudflare, aws, gcp, or null.',
108
+ 'config.firewall.cloudDryRun': 'Log intended cloud WAF pushes without applying them.',
109
+ 'config.firewall.cloudflare': 'Cloudflare API token/account id for Layer 4 WAF. Secrets: do not commit.',
110
+ 'config.firewall.aws': 'AWS WAF IP set configuration for Layer 4 WAF.',
111
+ 'config.firewall.gcp': 'GCP Cloud Armor policy configuration for Layer 4 WAF.',
112
+ 'config.networking.trustedProxies': 'Additional proxy IPs whose X-Forwarded-For headers should be trusted.',
113
+ });
114
+
115
+ const ENV_TO_CONFIG_PATH = Object.freeze({
116
+ SECURENOW_LOGGING_ENABLED: 'logging.enabled',
117
+ SECURENOW_CAPTURE_BODY: 'capture.body',
118
+ SECURENOW_CAPTURE_MULTIPART: 'capture.multipart',
119
+ SECURENOW_MAX_BODY_SIZE: 'capture.maxBodySize',
120
+ SECURENOW_SENSITIVE_FIELDS: 'capture.sensitiveFields',
121
+ SECURENOW_DISABLE_INSTRUMENTATIONS: 'otel.disableInstrumentations',
122
+ OTEL_EXPORTER_OTLP_ENDPOINT: 'otel.endpoint',
123
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'otel.tracesEndpoint',
124
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: 'otel.logsEndpoint',
125
+ OTEL_EXPORTER_OTLP_HEADERS: 'otel.headers',
126
+ OTEL_LOG_LEVEL: 'otel.logLevel',
127
+ SECURENOW_ENVIRONMENT: 'runtime.deploymentEnvironment',
128
+ SECURENOW_DEPLOYMENT_ENVIRONMENT: 'runtime.deploymentEnvironment',
129
+ NODE_ENV: 'runtime.deploymentEnvironment',
130
+ SECURENOW_NO_UUID: 'runtime.noUuid',
131
+ SECURENOW_STRICT: 'runtime.strict',
132
+ SECURENOW_TEST_SPAN: 'runtime.testSpan',
133
+ SECURENOW_HIDE_BANNER: 'runtime.hideBanner',
134
+ SECURENOW_FIREWALL_ENABLED: 'firewall.enabled',
135
+ SECURENOW_API_URL: 'firewall.apiUrl',
136
+ SECURENOW_FIREWALL_VERSION_INTERVAL: 'firewall.versionCheckInterval',
137
+ SECURENOW_FIREWALL_SYNC_INTERVAL: 'firewall.syncInterval',
138
+ SECURENOW_FIREWALL_FAIL_MODE: 'firewall.failMode',
139
+ SECURENOW_FIREWALL_STATUS_CODE: 'firewall.statusCode',
140
+ SECURENOW_FIREWALL_LOG: 'firewall.log',
141
+ SECURENOW_FIREWALL_TCP: 'firewall.tcp',
142
+ SECURENOW_FIREWALL_IPTABLES: 'firewall.iptables',
143
+ SECURENOW_FIREWALL_CLOUD: 'firewall.cloud',
144
+ SECURENOW_FIREWALL_CLOUD_DRY_RUN: 'firewall.cloudDryRun',
145
+ CLOUDFLARE_API_TOKEN: 'firewall.cloudflare.apiToken',
146
+ CLOUDFLARE_ACCOUNT_ID: 'firewall.cloudflare.accountId',
147
+ AWS_WAF_IP_SET_ID: 'firewall.aws.wafIpSetId',
148
+ AWS_WAF_IP_SET_NAME: 'firewall.aws.wafIpSetName',
149
+ AWS_WAF_SCOPE: 'firewall.aws.wafScope',
150
+ GCP_PROJECT_ID: 'firewall.gcp.projectId',
151
+ GCP_SECURITY_POLICY: 'firewall.gcp.securityPolicy',
152
+ SECURENOW_TRUSTED_PROXIES: 'networking.trustedProxies',
153
+ });
154
+
155
+ function clone(value) {
156
+ return value == null ? value : JSON.parse(JSON.stringify(value));
157
+ }
40
158
 
41
159
  function readJsonSafe(filepath) {
42
160
  try {
@@ -49,7 +167,7 @@ function readJsonSafe(filepath) {
49
167
  function loadLocalCredentials() {
50
168
  try {
51
169
  const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '.';
52
- return readJsonSafe(path.join(cwd, '.securenow', 'credentials.json'));
170
+ return withCredentialDefaults(readJsonSafe(path.join(cwd, '.securenow', 'credentials.json')));
53
171
  } catch {
54
172
  return null;
55
173
  }
@@ -57,14 +175,21 @@ function loadLocalCredentials() {
57
175
 
58
176
  function loadGlobalCredentials() {
59
177
  try {
60
- return readJsonSafe(path.join(os.homedir(), '.securenow', 'credentials.json'));
178
+ return withCredentialDefaults(readJsonSafe(path.join(os.homedir(), '.securenow', 'credentials.json')));
61
179
  } catch {
62
180
  return null;
63
181
  }
64
182
  }
65
183
 
66
184
  function loadCredentials() {
67
- return loadLocalCredentials() || loadGlobalCredentials() || null;
185
+ try {
186
+ const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '.';
187
+ const local = readJsonSafe(path.join(cwd, '.securenow', 'credentials.json'));
188
+ const global = readJsonSafe(path.join(os.homedir(), '.securenow', 'credentials.json'));
189
+ return withCredentialDefaults(mergeCredentials(global, local));
190
+ } catch {
191
+ return loadLocalCredentials() || loadGlobalCredentials() || null;
192
+ }
68
193
  }
69
194
 
70
195
  function loadPackageJsonName() {
@@ -78,70 +203,236 @@ function loadPackageJsonName() {
78
203
  return null;
79
204
  }
80
205
 
206
+ function mergeMissing(target, defaults) {
207
+ const out = target && typeof target === 'object' && !Array.isArray(target) ? clone(target) : {};
208
+ for (const [key, value] of Object.entries(defaults || {})) {
209
+ if (out[key] === undefined) {
210
+ out[key] = clone(value);
211
+ } else if (
212
+ value &&
213
+ typeof value === 'object' &&
214
+ !Array.isArray(value) &&
215
+ out[key] &&
216
+ typeof out[key] === 'object' &&
217
+ !Array.isArray(out[key])
218
+ ) {
219
+ out[key] = mergeMissing(out[key], value);
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+
225
+ function deepOverlay(base, overlay) {
226
+ if (!base || typeof base !== 'object' || Array.isArray(base)) base = {};
227
+ if (!overlay || typeof overlay !== 'object' || Array.isArray(overlay)) return clone(base);
228
+
229
+ const out = clone(base);
230
+ for (const [key, value] of Object.entries(overlay)) {
231
+ if (value === undefined) continue;
232
+ if (
233
+ value &&
234
+ typeof value === 'object' &&
235
+ !Array.isArray(value) &&
236
+ out[key] &&
237
+ typeof out[key] === 'object' &&
238
+ !Array.isArray(out[key])
239
+ ) {
240
+ out[key] = deepOverlay(out[key], value);
241
+ } else {
242
+ out[key] = clone(value);
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+
248
+ function mergeCredentials(globalCredentials, localCredentials) {
249
+ if (!globalCredentials && !localCredentials) return null;
250
+ return deepOverlay(globalCredentials || {}, localCredentials || {});
251
+ }
252
+
253
+ function withCredentialDefaults(credentials) {
254
+ if (!credentials || typeof credentials !== 'object') return null;
255
+ const out = clone(credentials);
256
+ out.config = mergeMissing(out.config, DEFAULT_CONFIG);
257
+ out._securenow = mergeMissing(out._securenow, {
258
+ schemaVersion: CONFIG_SCHEMA_VERSION,
259
+ note: 'Local SecureNow credentials and secure SDK defaults. This file may contain secrets; keep .securenow/ in .gitignore.',
260
+ precedence: 'The same credentials file works in local development and production. Legacy environment variables are only fallback inputs when this file does not define a value.',
261
+ explanations: CONFIG_EXPLANATIONS,
262
+ });
263
+ return out;
264
+ }
265
+
266
+ function getPath(obj, dotted) {
267
+ if (!obj || !dotted) return undefined;
268
+ let cur = obj;
269
+ for (const part of dotted.split('.')) {
270
+ if (!cur || typeof cur !== 'object' || !(part in cur)) return undefined;
271
+ cur = cur[part];
272
+ }
273
+ return cur;
274
+ }
275
+
276
+ function rawEnv(key) {
277
+ return process.env[key] ?? process.env[key.toUpperCase()] ?? process.env[key.toLowerCase()];
278
+ }
279
+
81
280
  function pick(value) {
82
281
  if (value === undefined || value === null) return null;
83
- const str = String(value).trim();
84
- return str.length > 0 ? str : null;
282
+ if (typeof value === 'string') {
283
+ const str = value.trim();
284
+ return str.length > 0 ? str : null;
285
+ }
286
+ return value;
85
287
  }
86
288
 
87
- // Routing key = app.key UUID. This is what the collector filters on.
88
- // Also used as x-api-key header once the collector supports it.
89
- function resolveAppKey() {
90
- const fromEnv =
91
- pick(process.env.SECURENOW_APPID) ||
92
- pick(process.env.SECURENOW_API_KEY) ||
93
- pick(process.env.securenow);
94
- if (fromEnv) return fromEnv;
289
+ function parseBool(value, fallback = false) {
290
+ if (value === undefined || value === null || value === '') return fallback;
291
+ if (typeof value === 'boolean') return value;
292
+ if (typeof value === 'number') return value !== 0;
293
+ const text = String(value).trim().toLowerCase();
294
+ if (['0', 'false', 'no', 'off', 'disabled'].includes(text)) return false;
295
+ if (['1', 'true', 'yes', 'on', 'enabled'].includes(text)) return true;
296
+ return fallback;
297
+ }
298
+
299
+ function parseNumber(value, fallback, min) {
300
+ const n = Number.parseInt(value, 10);
301
+ const resolved = Number.isFinite(n) ? n : fallback;
302
+ return typeof min === 'number' ? Math.max(min, resolved) : resolved;
303
+ }
304
+
305
+ function parseList(value) {
306
+ if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
307
+ if (value == null || value === '') return [];
308
+ return String(value).split(',').map((s) => s.trim()).filter(Boolean);
309
+ }
310
+
311
+ function normalizeDeploymentEnvironment(value) {
312
+ const raw = pick(value);
313
+ if (raw == null) return 'production';
314
+ const text = String(raw).trim().toLowerCase();
315
+ if (text === 'dev' || text === 'development') return 'local';
316
+ if (text === 'prod') return 'production';
317
+ if (!/^[a-z0-9_.-]{1,64}$/.test(text)) return 'production';
318
+ return text;
319
+ }
320
+
321
+ function parseHeaders(value) {
322
+ const out = {};
323
+ if (!value) return out;
324
+
325
+ if (typeof value === 'object' && !Array.isArray(value)) {
326
+ for (const [key, rawValue] of Object.entries(value)) {
327
+ const k = String(key).trim().toLowerCase();
328
+ const v = pick(rawValue);
329
+ if (k && v != null) out[k] = String(v);
330
+ }
331
+ return out;
332
+ }
333
+
334
+ for (const raw of String(value).split(',')) {
335
+ const s = raw.trim();
336
+ if (!s) continue;
337
+ const i = s.indexOf('=');
338
+ if (i === -1) continue;
339
+ const key = s.slice(0, i).trim().toLowerCase();
340
+ const val = s.slice(i + 1).trim();
341
+ if (key && val) out[key] = val;
342
+ }
343
+ return out;
344
+ }
345
+
346
+ function headersToString(headers) {
347
+ if (!headers || typeof headers !== 'object') return '';
348
+ return Object.entries(headers)
349
+ .filter(([, value]) => pick(value) != null)
350
+ .map(([key, value]) => `${key}=${value}`)
351
+ .join(',');
352
+ }
353
+
354
+ function toEnvString(value) {
355
+ if (value === undefined || value === null || value === '') return undefined;
356
+ if (typeof value === 'boolean') return value ? '1' : '0';
357
+ if (Array.isArray(value)) return value.join(',');
358
+ if (typeof value === 'object') return headersToString(value);
359
+ return String(value);
360
+ }
95
361
 
362
+ function resolveConfigPath(configPath, envKeys = [], fallback) {
96
363
  const creds = loadCredentials();
97
- if (creds && creds.app && pick(creds.app.key)) return pick(creds.app.key);
364
+ const fromCreds = pick(getPath(creds && creds.config, configPath));
365
+ if (fromCreds != null) return fromCreds;
366
+
367
+ for (const key of envKeys) {
368
+ const fromEnv = pick(rawEnv(key));
369
+ if (fromEnv != null) return fromEnv;
370
+ }
371
+
372
+ const fromDefault = pick(getPath(DEFAULT_CONFIG, configPath));
373
+ if (fromDefault != null) return fromDefault;
374
+
375
+ return fallback;
376
+ }
377
+
378
+ function resolveAppKey() {
379
+ const creds = loadCredentials();
380
+ if (creds && creds.app && pick(creds.app.key)) return String(pick(creds.app.key));
381
+
382
+ const fromEnv = pick(rawEnv('SECURENOW_APPID')) || pick(rawEnv('securenow'));
383
+ if (fromEnv) return String(fromEnv);
384
+
385
+ // Legacy compatibility: older docs sometimes used SECURENOW_API_KEY as the
386
+ // app routing UUID. Real firewall keys start with snk_live_ and are handled
387
+ // by resolveApiKey(), not as service.name.
388
+ const legacyApiKey = pick(rawEnv('SECURENOW_API_KEY'));
389
+ if (legacyApiKey && !String(legacyApiKey).startsWith('snk_live_')) {
390
+ return String(legacyApiKey);
391
+ }
392
+
98
393
  return null;
99
394
  }
100
395
 
101
- // Human label for observability tools — never touches collector routing.
102
- // Falls back to package.json#name so pre-login users still see something readable.
103
396
  function resolveAppName() {
104
- const fromEnv = pick(process.env.OTEL_SERVICE_NAME);
105
- if (fromEnv) return fromEnv;
106
-
107
397
  const creds = loadCredentials();
108
- if (creds && creds.app && pick(creds.app.name)) return pick(creds.app.name);
398
+ if (creds && creds.app && pick(creds.app.name)) return String(pick(creds.app.name));
399
+
400
+ const fromEnv = pick(rawEnv('OTEL_SERVICE_NAME'));
401
+ if (fromEnv) return String(fromEnv);
109
402
 
110
403
  return loadPackageJsonName();
111
404
  }
112
405
 
113
- // Legacy alias — callers that want "whatever identifies this process to OTel"
114
- // get the routing key if logged in, otherwise the human label fallback.
115
406
  function resolveAppId() {
116
407
  return resolveAppKey() || resolveAppName();
117
408
  }
118
409
 
119
- // Firewall / user-scoped API key (snk_live_...).
120
- // Distinct from resolveAppKey: that one is the application UUID for OTel
121
- // routing. This one authenticates the firewall blocklist sync to /api/firewall,
122
- // so it must look like a real `snk_live_` key — the app UUID won't pass auth.
123
410
  function resolveApiKey() {
124
- const fromEnv = pick(process.env.SECURENOW_API_KEY);
125
- if (fromEnv && fromEnv.startsWith('snk_live_')) return fromEnv;
126
-
127
411
  const creds = loadCredentials();
128
412
  const fromCreds = creds && pick(creds.apiKey);
129
- if (fromCreds && fromCreds.startsWith('snk_live_')) return fromCreds;
413
+ if (fromCreds && String(fromCreds).startsWith('snk_live_')) return String(fromCreds);
414
+
415
+ const fromEnv = pick(rawEnv('SECURENOW_API_KEY'));
416
+ if (fromEnv && String(fromEnv).startsWith('snk_live_')) return String(fromEnv);
130
417
 
131
418
  return null;
132
419
  }
133
420
 
134
421
  function resolveInstance() {
135
- const fromEnv =
136
- pick(process.env.SECURENOW_INSTANCE) ||
137
- pick(process.env.securenow_instance) ||
138
- pick(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
139
- if (fromEnv) return fromEnv.replace(/\/$/, '');
422
+ const fromConfig = pick(resolveConfigPath('otel.endpoint'));
423
+ if (fromConfig) return String(fromConfig).replace(/\/$/, '');
140
424
 
141
425
  const creds = loadCredentials();
142
426
  if (creds && creds.app && pick(creds.app.instance)) {
143
- return pick(creds.app.instance).replace(/\/$/, '');
427
+ return String(pick(creds.app.instance)).replace(/\/$/, '');
144
428
  }
429
+
430
+ const fromEnv =
431
+ pick(rawEnv('SECURENOW_INSTANCE')) ||
432
+ pick(rawEnv('securenow_instance')) ||
433
+ pick(rawEnv('OTEL_EXPORTER_OTLP_ENDPOINT'));
434
+ if (fromEnv) return String(fromEnv).replace(/\/$/, '');
435
+
145
436
  return FREE_TRIAL_INSTANCE;
146
437
  }
147
438
 
@@ -150,46 +441,151 @@ function resolveAll() {
150
441
  return {
151
442
  appKey,
152
443
  appName: resolveAppName(),
153
- // service.name must be the routing UUID when available — that's what the
154
- // collector filters on. Fall back to the human label pre-login.
155
444
  appId: appKey || resolveAppName(),
156
445
  instance: resolveInstance(),
446
+ deploymentEnvironment: resolveDeploymentEnvironment(),
157
447
  };
158
448
  }
159
449
 
160
- // Decide whether to append a per-worker UUID suffix to service.name.
161
- //
162
- // The dashboard filters traces with an exact match on service.name
163
- // (`resource_string_service$$name IN (...)`), so when appId IS the routing
164
- // UUID (customer is logged in → credentials file provides app.key), the
165
- // suffix guarantees a miss. Per-worker disambiguation still happens via
166
- // service.instance.id, which is never used for filtering.
167
- //
168
- // Precedence:
169
- // 1. Explicit opts.noUuid (caller passed it) — wins
170
- // 2. Explicit SECURENOW_NO_UUID env var (0/1/true/false) — wins
171
- // 3. appKey resolved (logged-in, appId is routing UUID) → true
172
- // 4. Otherwise (pre-login, appId = package.json#name) → false
450
+ function resolveDeploymentEnvironment() {
451
+ return normalizeDeploymentEnvironment(
452
+ resolveConfigPath('runtime.deploymentEnvironment', [
453
+ 'SECURENOW_ENVIRONMENT',
454
+ 'SECURENOW_DEPLOYMENT_ENVIRONMENT',
455
+ 'NODE_ENV',
456
+ ])
457
+ );
458
+ }
459
+
173
460
  function resolveNoUuid(opts = {}) {
174
461
  if (opts.noUuid !== undefined && opts.noUuid !== null) return !!opts.noUuid;
175
462
 
176
- const raw = process.env.SECURENOW_NO_UUID;
177
- if (raw !== undefined && raw !== '') {
178
- return /^(1|true)$/i.test(String(raw).trim());
179
- }
463
+ const fromConfig = pick(getPath(loadCredentials()?.config, 'runtime.noUuid'));
464
+ if (fromConfig !== null) return parseBool(fromConfig, false);
465
+
466
+ const raw = pick(rawEnv('SECURENOW_NO_UUID'));
467
+ if (raw !== null) return parseBool(raw, false);
180
468
 
181
469
  return !!resolveAppKey();
182
470
  }
183
471
 
472
+ function resolveEnvKey(key) {
473
+ const upper = String(key).toUpperCase();
474
+
475
+ if (upper === 'SECURENOW_APPID') return resolveAppKey();
476
+ if (upper === 'OTEL_SERVICE_NAME') return resolveAppName();
477
+ if (upper === 'SECURENOW_API_KEY') return resolveApiKey();
478
+ if (upper === 'SECURENOW_INSTANCE' || upper === 'OTEL_EXPORTER_OTLP_ENDPOINT') return resolveInstance();
479
+
480
+ const configPath = ENV_TO_CONFIG_PATH[upper];
481
+ if (configPath) return resolveConfigPath(configPath);
482
+
483
+ const fromEnv = pick(rawEnv(upper));
484
+ if (fromEnv != null) return fromEnv;
485
+
486
+ return undefined;
487
+ }
488
+
489
+ function env(key) {
490
+ return toEnvString(resolveEnvKey(key));
491
+ }
492
+
493
+ function boolEnv(key, fallback = false) {
494
+ return parseBool(resolveEnvKey(key), fallback);
495
+ }
496
+
497
+ function numberEnv(key, fallback, min) {
498
+ return parseNumber(resolveEnvKey(key), fallback, min);
499
+ }
500
+
501
+ function listEnv(key) {
502
+ return parseList(resolveEnvKey(key));
503
+ }
504
+
505
+ function resolveOtlpHeaders() {
506
+ const headers = parseHeaders(resolveConfigPath('otel.headers', ['OTEL_EXPORTER_OTLP_HEADERS'], {}));
507
+ const appKey = resolveAppKey();
508
+ if (appKey && !headers['x-api-key']) {
509
+ headers['x-api-key'] = appKey;
510
+ }
511
+ return headers;
512
+ }
513
+
514
+ function resolveOtlpHeaderString() {
515
+ return headersToString(resolveOtlpHeaders());
516
+ }
517
+
518
+ function resolveEndpoints(options = {}) {
519
+ const endpointBase = String(options.endpoint || resolveInstance()).replace(/\/$/, '');
520
+ return {
521
+ endpointBase,
522
+ tracesUrl: env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`,
523
+ logsUrl: env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`,
524
+ headers: resolveOtlpHeaders(),
525
+ };
526
+ }
527
+
528
+ function resolveFirewallOptions() {
529
+ return {
530
+ apiKey: resolveApiKey(),
531
+ appKey: resolveAppKey() || null,
532
+ environment: resolveDeploymentEnvironment(),
533
+ enabled: boolEnv('SECURENOW_FIREWALL_ENABLED', true),
534
+ apiUrl: env('SECURENOW_API_URL') || DEFAULT_API_URL,
535
+ versionCheckInterval: numberEnv('SECURENOW_FIREWALL_VERSION_INTERVAL', 10, 1),
536
+ syncInterval: numberEnv('SECURENOW_FIREWALL_SYNC_INTERVAL', 300, 1),
537
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
538
+ statusCode: numberEnv('SECURENOW_FIREWALL_STATUS_CODE', 403, 100),
539
+ log: boolEnv('SECURENOW_FIREWALL_LOG', true),
540
+ tcp: boolEnv('SECURENOW_FIREWALL_TCP', false),
541
+ iptables: boolEnv('SECURENOW_FIREWALL_IPTABLES', false),
542
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
543
+ cloudDryRun: boolEnv('SECURENOW_FIREWALL_CLOUD_DRY_RUN', false),
544
+ cloudflare: {
545
+ apiToken: env('CLOUDFLARE_API_TOKEN') || null,
546
+ accountId: env('CLOUDFLARE_ACCOUNT_ID') || null,
547
+ },
548
+ aws: {
549
+ wafIpSetId: env('AWS_WAF_IP_SET_ID') || null,
550
+ wafIpSetName: env('AWS_WAF_IP_SET_NAME') || 'securenow-blocklist',
551
+ wafScope: env('AWS_WAF_SCOPE') || 'REGIONAL',
552
+ },
553
+ gcp: {
554
+ projectId: env('GCP_PROJECT_ID') || null,
555
+ securityPolicy: env('GCP_SECURITY_POLICY') || null,
556
+ },
557
+ };
558
+ }
559
+
184
560
  module.exports = {
185
561
  FREE_TRIAL_INSTANCE,
562
+ DEFAULT_API_URL,
563
+ CONFIG_SCHEMA_VERSION,
564
+ DEFAULT_CONFIG,
565
+ CONFIG_EXPLANATIONS,
566
+ ENV_TO_CONFIG_PATH,
567
+ withCredentialDefaults,
568
+ mergeCredentials,
569
+ resolveConfigPath,
186
570
  resolveAppKey,
187
571
  resolveAppName,
188
572
  resolveAppId,
189
573
  resolveApiKey,
190
574
  resolveInstance,
575
+ normalizeDeploymentEnvironment,
576
+ resolveDeploymentEnvironment,
191
577
  resolveAll,
192
578
  resolveNoUuid,
579
+ resolveOtlpHeaders,
580
+ resolveOtlpHeaderString,
581
+ resolveEndpoints,
582
+ resolveFirewallOptions,
583
+ env,
584
+ boolEnv,
585
+ numberEnv,
586
+ listEnv,
587
+ parseHeaders,
588
+ headersToString,
193
589
  loadCredentials,
194
590
  loadLocalCredentials,
195
591
  loadGlobalCredentials,
package/cli/apiKey.js CHANGED
@@ -45,7 +45,7 @@ async function clear(args, flags) {
45
45
  async function show() {
46
46
  const key = config.getApiKey();
47
47
  if (!key) {
48
- ui.info('No API key stored in credentials. Falling back to SECURENOW_API_KEY env var.');
48
+ ui.info('No API key stored in credentials. Run `npx securenow login` or `npx securenow api-key set snk_live_...`.');
49
49
  return;
50
50
  }
51
51
  console.log(maskKey(key));
package/cli/apps.js CHANGED
@@ -242,7 +242,7 @@ async function info(args, flags) {
242
242
 
243
243
  console.log('');
244
244
  console.log(` ${ui.c.bold('Use in current project:')} ${ui.c.bold(`securenow apps default ${app.key}`)}`);
245
- console.log(` ${ui.c.bold('Or override via env:')} ${ui.c.dim(`SECURENOW_APPID=${app.key} SECURENOW_INSTANCE=${envUrl}`)}`);
245
+ console.log(` ${ui.c.bold('Or for CI/prod env:')} ${ui.c.dim(`SECURENOW_APPID=${app.key} SECURENOW_INSTANCE=${envUrl}`)}`);
246
246
  console.log('');
247
247
  } catch (err) {
248
248
  s.fail('Failed to fetch application');