securenow 7.7.13 → 7.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL-CLI.md CHANGED
@@ -35,13 +35,13 @@ securenow login --token <JWT> # headless / CI login (get token from dashboard
35
35
  securenow whoami # verify session (shows email, app, auth source)
36
36
  ```
37
37
 
38
- **Zero-config flow (v7+):** the browser step lets the user pick (or create) an app. The CLI stores the app's **key (UUID)**, **name**, and **instance URL** in `.securenow/credentials.json`. The SDK reads this file at boot and sends traces/logs to the right app bucket — **no env vars required for local dev or production**.
38
+ **Zero-config flow (v7+):** the browser step lets the user pick (or create) an app. The CLI stores the app's **key (UUID)** and **name** in `.securenow/credentials.json`. The SDK sends traces/logs to the default SecureNow ingestion gateway, which routes by app key — **no env vars or per-instance collector URLs required for local dev or production**.
39
39
 
40
40
  **Default-on security (v7.5.1+):** after picking or creating the app, `securenow login` turns on that app's firewall toggle, mints an API key with `firewall:read + blocklist:read + allowlist:read` scopes, and writes it into `.securenow/credentials.json`. Traces, logs, POST body capture, multipart metadata capture, and the firewall are enabled by default. No `SECURENOW_API_KEY` env var is needed. To add or rotate a key later without re-running login, use `securenow api-key set snk_live_...` (see [API Key Management](#api-key-management) below).
41
41
 
42
- Credentials resolve in order: project `.securenow/credentials.json` -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> global `~/.securenow/credentials.json` -> global named runtime credentials in the same fixed order. Legacy env vars are fallback-only for existing deployments and do not choose the credentials filename.
42
+ Credentials resolve in order: project `.securenow/credentials.json` -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> global `~/.securenow/credentials.json` -> global named runtime credentials in the same fixed order. Runtime config is credentials-json based; legacy env fallbacks are disabled unless `SECURENOW_ENABLE_LEGACY_ENV=1` is set and never choose the credentials filename.
43
43
 
44
- The **firewall API key** should live in the same credentials file as `apiKey`. Legacy `SECURENOW_API_KEY` overrides are honored only when they start with `snk_live_`.
44
+ The **firewall API key** should live in the same credentials file as `apiKey`.
45
45
 
46
46
  For CI / Docker / production, use `securenow credentials runtime --env production` to generate a tokenless runtime file, then mount/copy it as `.securenow/credentials.json`. Since v7.7.2, mounting the generated `.securenow/credentials.production.json` filename directly also works when `credentials.json` is absent.
47
47
 
@@ -83,7 +83,7 @@ Config lives in `~/.securenow/` (global) and optionally `.securenow/` (per-proje
83
83
  | `.securenow/credentials.json` | `token`, `email`, `expiresAt`, `apiKey`, `app`, `config`, `_securenow.explanations` (project-local default) |
84
84
  | `.securenow/credentials.<environment>.json` | Tokenless runtime credentials generated by `securenow credentials runtime --env <environment>`; read in a fixed order, not selected from env vars |
85
85
 
86
- **Credential resolution order:** `.securenow/credentials.json` (project) -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> `~/.securenow/credentials.json` (global) -> global named runtime credentials in the same fixed order. Legacy env vars are fallback-only for existing deployments.
86
+ **Credential resolution order:** `.securenow/credentials.json` (project) -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> `~/.securenow/credentials.json` (global) -> global named runtime credentials in the same fixed order. Legacy env fallbacks are disabled unless `SECURENOW_ENABLE_LEGACY_ENV=1` is set.
87
87
 
88
88
  **Firewall API key resolution (v7.5.1+):** project `.securenow/credentials.json` -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> global `~/.securenow/credentials.json` -> global named runtime credentials in the same fixed order. Use `securenow login` for default setup or `securenow api-key set` to rotate a key without touching env vars.
89
89
 
@@ -95,7 +95,7 @@ securenow config get defaultApp # show one
95
95
  securenow config path # print file paths + active auth source
96
96
  ```
97
97
 
98
- Legacy CLI overrides still exist for existing automation (`SECURENOW_TOKEN`, `SECURENOW_API_URL`, `SECURENOW_APP_URL`, `SECURENOW_APP`), but file credentials are the default path.
98
+ Legacy CLI overrides still exist for operator automation, but runtime SDK config should stay in the credentials file.
99
99
 
100
100
  ## Global Flags
101
101
 
@@ -172,8 +172,8 @@ securenow init [--env local] [--key <API_KEY>]
172
172
  ```
173
173
 
174
174
  Auto-detects framework (Next.js, Nuxt, Express, Fastify, Koa, Hapi, Node) from `package.json`. Then:
175
- - **Credentials**: ensures `.securenow/credentials.json` exists, has secure defaults/explanations, and `.securenow/` is gitignored
176
- - **Next.js**: creates `instrumentation.ts/js` with `securenow/nextjs` + `securenow/nextjs-auto-capture`, and adds `serverExternalPackages: ['securenow']` when the config can be patched safely
175
+ - **Credentials**: ensures `.securenow/credentials.json` exists, has secure defaults/explanations, and local credential JSON files are gitignored without ignoring the whole `.securenow/` directory
176
+ - **Next.js**: creates `instrumentation.ts/js` with `securenow/nextjs` + `securenow/nextjs-auto-capture`, and adds `serverExternalPackages: ['securenow']` plus `outputFileTracingIncludes` when the config can be patched safely
177
177
  - **Existing Next.js files**: prints a Codex/Claude-ready merge prompt when instrumentation or config already exists or is too custom to safely patch
178
178
  - **Nuxt**: tells you to add `securenow/nuxt` to modules
179
179
  - **Node/Express/etc.**: suggests adding `-r securenow/register` to start script
@@ -473,7 +473,7 @@ securenow test-span
473
473
  securenow test-span "ci.smoke-test" # custom span name
474
474
  ```
475
475
 
476
- Both commands use the resolved collector settings from `.securenow/credentials.json` (`app.instance`, `config.otel.*`) and return non-zero on HTTP errors so CI/cron can detect failures.
476
+ Both commands use the default SecureNow ingestion gateway plus optional advanced `config.otel.*` overrides from `.securenow/credentials.json`, and return non-zero on HTTP errors so CI/cron can detect failures.
477
477
 
478
478
  ### Utilities — Redaction, CIDR, Diagnostics
479
479
 
@@ -497,7 +497,7 @@ securenow doctor # exits 0 if healthy, 1 otherwise
497
497
  securenow doctor --json
498
498
  ```
499
499
 
500
- The `redact` command accepts a JSON string or `@path/to/file.json`, layers your `--fields` flag on top of `DEFAULT_SENSITIVE_FIELDS`, and also honors `SECURENOW_SENSITIVE_FIELDS` from the env. Exit code from `cidr match` is `0` if the IP matches the list, `2` otherwise — scriptable.
500
+ The `redact` command accepts a JSON string or `@path/to/file.json` and layers your `--fields` flag on top of `DEFAULT_SENSITIVE_FIELDS`. Exit code from `cidr match` is `0` if the IP matches the list, `2` otherwise — scriptable.
501
501
 
502
502
  ---
503
503
 
package/app-config.js CHANGED
@@ -8,17 +8,18 @@
8
8
  * ./.securenow/credentials.production.json are also accepted when the
9
9
  * canonical file is not present. Filename selection is deterministic and does
10
10
  * not read environment variables.
11
- * Legacy environment variables are only fallback inputs for existing installs;
12
- * every SDK setting has a file-backed equivalent so customers do not need .env
13
- * files.
11
+ * Legacy environment fallbacks are disabled by default; every SDK setting has
12
+ * a file-backed equivalent so customers do not need .env files.
14
13
  */
15
14
 
16
15
  const fs = require('fs');
17
16
  const path = require('path');
18
17
  const os = require('os');
19
18
 
20
- const FREE_TRIAL_INSTANCE = 'https://freetrial.securenow.ai:4318';
19
+ const FREE_TRIAL_INSTANCE = 'https://ingest.securenow.ai';
21
20
  const DEFAULT_API_URL = 'https://api.securenow.ai';
21
+ const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
22
+ const LEGACY_ENV_FALLBACK_FLAG = 'SECURENOW_ENABLE_LEGACY_ENV';
22
23
  const CONFIG_SCHEMA_VERSION = 2;
23
24
  const CREDENTIAL_FILE_ENVIRONMENTS = Object.freeze([
24
25
  'staging',
@@ -92,13 +93,13 @@ const CONFIG_EXPLANATIONS = Object.freeze({
92
93
  'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by login or `securenow api-key set`. Secret: do not commit.',
93
94
  'app.key': 'SecureNow application routing UUID. The SDK uses this as OTel service.name so dashboard queries match exactly.',
94
95
  'app.name': 'Human-readable app label shown in CLI output.',
95
- 'app.instance': 'OTLP collector base URL for this app. Production should provide this same credentials file at .securenow/credentials.json.',
96
+ 'app.routing': 'Telemetry uses the default SecureNow ingestion gateway and routes by app.key; runtime credentials do not expose per-instance collector URLs.',
96
97
  'config.logging.enabled': 'Secure default: console log forwarding is on. Set false to disable OTLP logs.',
97
98
  'config.capture.body': 'Secure default: JSON, GraphQL, and form request bodies are captured with redaction.',
98
99
  'config.capture.multipart': 'Secure default: multipart text fields and file metadata are captured. File content is never buffered.',
99
100
  'config.capture.maxBodySize': 'Maximum body bytes captured per request. Default 10KB limits memory and sensitive data exposure.',
100
101
  'config.capture.sensitiveFields': 'Extra field-name fragments to redact in addition to SecureNow built-ins.',
101
- 'config.otel.endpoint': 'Optional OTLP base endpoint override. Usually app.instance is enough.',
102
+ 'config.otel.endpoint': 'Optional OTLP base endpoint override for advanced/self-hosted collectors. Leave null for the SecureNow ingestion gateway.',
102
103
  'config.otel.tracesEndpoint': 'Optional full traces endpoint override, for split collectors.',
103
104
  'config.otel.logsEndpoint': 'Optional full logs endpoint override, for split collectors.',
104
105
  'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-api-key from app.key when missing.',
@@ -392,11 +393,14 @@ function mergeCredentials(globalCredentials, localCredentials) {
392
393
  function withCredentialDefaults(credentials) {
393
394
  if (!credentials || typeof credentials !== 'object') return null;
394
395
  const out = clone(credentials);
396
+ if (out.app && typeof out.app === 'object' && !Array.isArray(out.app)) {
397
+ delete out.app.instance;
398
+ }
395
399
  out.config = mergeMissing(out.config, DEFAULT_CONFIG);
396
400
  out._securenow = mergeMissing(out._securenow, {
397
401
  schemaVersion: CONFIG_SCHEMA_VERSION,
398
402
  note: 'Local SecureNow credentials and secure SDK defaults. This file may contain secrets; keep .securenow/ in .gitignore.',
399
- 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.',
403
+ precedence: `The same credentials file works in local development and production. SDK runtime config is read from credentials JSON. Legacy environment fallbacks are disabled unless ${LEGACY_ENV_FALLBACK_FLAG}=1 is set.`,
400
404
  explanations: CONFIG_EXPLANATIONS,
401
405
  });
402
406
  return out;
@@ -412,10 +416,47 @@ function getPath(obj, dotted) {
412
416
  return cur;
413
417
  }
414
418
 
419
+ function legacyEnvFallbackEnabled() {
420
+ const raw =
421
+ process.env[LEGACY_ENV_FALLBACK_FLAG] ??
422
+ process.env.SECURENOW_ALLOW_ENV_CONFIG ??
423
+ process.env.SECURENOW_LEGACY_ENV;
424
+ return /^(1|true|yes)$/i.test(String(raw || '').trim());
425
+ }
426
+
415
427
  function rawEnv(key) {
428
+ if (!legacyEnvFallbackEnabled()) return undefined;
416
429
  return process.env[key] ?? process.env[key.toUpperCase()] ?? process.env[key.toLowerCase()];
417
430
  }
418
431
 
432
+ function normalizeInstanceEndpoint(value) {
433
+ if (value === undefined || value === null || value === '') return value;
434
+ const endpoint = String(value).trim().replace(/\/$/, '');
435
+ if (!endpoint) return endpoint;
436
+ if (endpoint === LEGACY_SECURENOW_GATEWAY) return FREE_TRIAL_INSTANCE;
437
+
438
+ try {
439
+ const parseable = /^[a-z][a-z0-9+.-]*:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`;
440
+ const url = new URL(parseable);
441
+ if (url.port === '4318' && url.hostname.toLowerCase().endsWith('.securenow.ai')) {
442
+ return FREE_TRIAL_INSTANCE;
443
+ }
444
+ } catch {}
445
+
446
+ return endpoint;
447
+ }
448
+
449
+ function normalizeSignalEndpoint(value, signalType) {
450
+ if (value === undefined || value === null || value === '') return value;
451
+ const endpoint = String(value).trim().replace(/\/$/, '');
452
+ const signalPath = `/v1/${signalType}`;
453
+ if (endpoint.endsWith(signalPath)) {
454
+ const base = normalizeInstanceEndpoint(endpoint.slice(0, -signalPath.length));
455
+ return `${base}${signalPath}`;
456
+ }
457
+ return endpoint;
458
+ }
459
+
419
460
  function pick(value) {
420
461
  if (value === undefined || value === null) return null;
421
462
  if (typeof value === 'string') {
@@ -514,6 +555,22 @@ function resolveConfigPath(configPath, envKeys = [], fallback) {
514
555
  return fallback;
515
556
  }
516
557
 
558
+ function configValue(configPath, fallback) {
559
+ return resolveConfigPath(configPath, [], fallback);
560
+ }
561
+
562
+ function boolConfig(configPath, fallback = false) {
563
+ return parseBool(configValue(configPath), fallback);
564
+ }
565
+
566
+ function numberConfig(configPath, fallback, min) {
567
+ return parseNumber(configValue(configPath), fallback, min);
568
+ }
569
+
570
+ function listConfig(configPath) {
571
+ return parseList(configValue(configPath));
572
+ }
573
+
517
574
  function resolveAppKey() {
518
575
  const creds = loadCredentials();
519
576
  if (creds && creds.app && pick(creds.app.key)) return String(pick(creds.app.key));
@@ -559,18 +616,13 @@ function resolveApiKey() {
559
616
 
560
617
  function resolveInstance() {
561
618
  const fromConfig = pick(resolveConfigPath('otel.endpoint'));
562
- if (fromConfig) return String(fromConfig).replace(/\/$/, '');
563
-
564
- const creds = loadCredentials();
565
- if (creds && creds.app && pick(creds.app.instance)) {
566
- return String(pick(creds.app.instance)).replace(/\/$/, '');
567
- }
619
+ if (fromConfig) return normalizeInstanceEndpoint(fromConfig);
568
620
 
569
621
  const fromEnv =
570
622
  pick(rawEnv('SECURENOW_INSTANCE')) ||
571
623
  pick(rawEnv('securenow_instance')) ||
572
624
  pick(rawEnv('OTEL_EXPORTER_OTLP_ENDPOINT'));
573
- if (fromEnv) return String(fromEnv).replace(/\/$/, '');
625
+ if (fromEnv) return normalizeInstanceEndpoint(fromEnv);
574
626
 
575
627
  return FREE_TRIAL_INSTANCE;
576
628
  }
@@ -587,13 +639,7 @@ function resolveAll() {
587
639
  }
588
640
 
589
641
  function resolveDeploymentEnvironment() {
590
- return normalizeDeploymentEnvironment(
591
- resolveConfigPath('runtime.deploymentEnvironment', [
592
- 'SECURENOW_ENVIRONMENT',
593
- 'SECURENOW_DEPLOYMENT_ENVIRONMENT',
594
- 'NODE_ENV',
595
- ])
596
- );
642
+ return normalizeDeploymentEnvironment(resolveConfigPath('runtime.deploymentEnvironment'));
597
643
  }
598
644
 
599
645
  function resolveNoUuid(opts = {}) {
@@ -643,7 +689,7 @@ function listEnv(key) {
643
689
  }
644
690
 
645
691
  function resolveOtlpHeaders() {
646
- const headers = parseHeaders(resolveConfigPath('otel.headers', ['OTEL_EXPORTER_OTLP_HEADERS'], {}));
692
+ const headers = parseHeaders(configValue('otel.headers', {}));
647
693
  const appKey = resolveAppKey();
648
694
  if (appKey && !headers['x-api-key']) {
649
695
  headers['x-api-key'] = appKey;
@@ -656,11 +702,13 @@ function resolveOtlpHeaderString() {
656
702
  }
657
703
 
658
704
  function resolveEndpoints(options = {}) {
659
- const endpointBase = String(options.endpoint || resolveInstance()).replace(/\/$/, '');
705
+ const endpointBase = String(normalizeInstanceEndpoint(options.endpoint || resolveInstance())).replace(/\/$/, '');
706
+ const tracesOverride = configValue('otel.tracesEndpoint');
707
+ const logsOverride = configValue('otel.logsEndpoint');
660
708
  return {
661
709
  endpointBase,
662
- tracesUrl: env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`,
663
- logsUrl: env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`,
710
+ tracesUrl: normalizeSignalEndpoint(tracesOverride, 'traces') || `${endpointBase}/v1/traces`,
711
+ logsUrl: normalizeSignalEndpoint(logsOverride, 'logs') || `${endpointBase}/v1/logs`,
664
712
  headers: resolveOtlpHeaders(),
665
713
  };
666
714
  }
@@ -678,28 +726,28 @@ function resolveFirewallOptions() {
678
726
  appKey: resolveAppKey() || null,
679
727
  environment: resolveDeploymentEnvironment(),
680
728
  enabled: resolveFirewallEnabled(),
681
- apiUrl: env('SECURENOW_API_URL') || DEFAULT_API_URL,
682
- versionCheckInterval: numberEnv('SECURENOW_FIREWALL_VERSION_INTERVAL', 10, 1),
683
- syncInterval: numberEnv('SECURENOW_FIREWALL_SYNC_INTERVAL', 3600, 1),
684
- failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
685
- statusCode: numberEnv('SECURENOW_FIREWALL_STATUS_CODE', 403, 100),
686
- log: boolEnv('SECURENOW_FIREWALL_LOG', true),
687
- tcp: boolEnv('SECURENOW_FIREWALL_TCP', false),
688
- iptables: boolEnv('SECURENOW_FIREWALL_IPTABLES', false),
689
- cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
690
- cloudDryRun: boolEnv('SECURENOW_FIREWALL_CLOUD_DRY_RUN', false),
729
+ apiUrl: configValue('firewall.apiUrl', DEFAULT_API_URL) || DEFAULT_API_URL,
730
+ versionCheckInterval: numberConfig('firewall.versionCheckInterval', 10, 1),
731
+ syncInterval: numberConfig('firewall.syncInterval', 3600, 1),
732
+ failMode: configValue('firewall.failMode', 'open') || 'open',
733
+ statusCode: numberConfig('firewall.statusCode', 403, 100),
734
+ log: boolConfig('firewall.log', true),
735
+ tcp: boolConfig('firewall.tcp', false),
736
+ iptables: boolConfig('firewall.iptables', false),
737
+ cloud: configValue('firewall.cloud', null) || null,
738
+ cloudDryRun: boolConfig('firewall.cloudDryRun', false),
691
739
  cloudflare: {
692
- apiToken: env('CLOUDFLARE_API_TOKEN') || null,
693
- accountId: env('CLOUDFLARE_ACCOUNT_ID') || null,
740
+ apiToken: resolveConfigPath('firewall.cloudflare.apiToken', [], null) || null,
741
+ accountId: resolveConfigPath('firewall.cloudflare.accountId', [], null) || null,
694
742
  },
695
743
  aws: {
696
- wafIpSetId: env('AWS_WAF_IP_SET_ID') || null,
697
- wafIpSetName: env('AWS_WAF_IP_SET_NAME') || 'securenow-blocklist',
698
- wafScope: env('AWS_WAF_SCOPE') || 'REGIONAL',
744
+ wafIpSetId: resolveConfigPath('firewall.aws.wafIpSetId', [], null) || null,
745
+ wafIpSetName: resolveConfigPath('firewall.aws.wafIpSetName', [], 'securenow-blocklist') || 'securenow-blocklist',
746
+ wafScope: resolveConfigPath('firewall.aws.wafScope', [], 'REGIONAL') || 'REGIONAL',
699
747
  },
700
748
  gcp: {
701
- projectId: env('GCP_PROJECT_ID') || null,
702
- securityPolicy: env('GCP_SECURITY_POLICY') || null,
749
+ projectId: resolveConfigPath('firewall.gcp.projectId', [], null) || null,
750
+ securityPolicy: resolveConfigPath('firewall.gcp.securityPolicy', [], null) || null,
703
751
  },
704
752
  };
705
753
  }
@@ -707,6 +755,8 @@ function resolveFirewallOptions() {
707
755
  module.exports = {
708
756
  FREE_TRIAL_INSTANCE,
709
757
  DEFAULT_API_URL,
758
+ LEGACY_SECURENOW_GATEWAY,
759
+ LEGACY_ENV_FALLBACK_FLAG,
710
760
  CONFIG_SCHEMA_VERSION,
711
761
  CREDENTIAL_FILE_ENVIRONMENTS,
712
762
  DEFAULT_CONFIG,
@@ -715,12 +765,17 @@ module.exports = {
715
765
  withCredentialDefaults,
716
766
  mergeCredentials,
717
767
  resolveConfigPath,
768
+ configValue,
769
+ boolConfig,
770
+ numberConfig,
771
+ listConfig,
718
772
  resolveAppKey,
719
773
  resolveAppName,
720
774
  resolveAppId,
721
775
  resolveApiKey,
722
776
  resolveInstance,
723
777
  normalizeDeploymentEnvironment,
778
+ legacyEnvFallbackEnabled,
724
779
  resolveDeploymentEnvironment,
725
780
  resolveAll,
726
781
  resolveNoUuid,
@@ -734,6 +789,8 @@ module.exports = {
734
789
  numberEnv,
735
790
  listEnv,
736
791
  parseHeaders,
792
+ normalizeInstanceEndpoint,
793
+ normalizeSignalEndpoint,
737
794
  headersToString,
738
795
  credentialRelativePaths,
739
796
  resolveLocalCredentialsFile,