securenow 7.7.15 → 7.8.1

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
@@ -24,24 +24,27 @@ codex mcp add securenow -- npx securenow mcp
24
24
  npx -p securenow securenow-mcp
25
25
  ```
26
26
 
27
- The MCP server reuses `.securenow/credentials.json` and exposes apps, traces, logs, firewall, IP intelligence, forensics, notifications, remediation tools, alert-rule review/tuning tools, bundled docs resources, and setup prompts. Write tools require `confirm:true` plus a reason.
27
+ The MCP server reads admin/control-plane auth from `.securenow/admin.json` and SDK runtime app credentials from `.securenow/runtime.json`, with legacy `.securenow/credentials.json` still supported. Admin/global tools use admin auth; runtime-scoped read tools can use the runtime firewall key where its scopes allow it. Write tools require `confirm:true` plus a reason.
28
28
 
29
29
  ### Authenticate
30
30
 
31
31
  ```bash
32
- securenow login # opens browser OAuth + app picker; writes ./.securenow/credentials.json (project-local by default)
33
- securenow login --global # save to ~/.securenow/ instead (shared across projects)
34
- securenow login --token <JWT> # headless / CI login (get token from dashboard Settings)
35
- securenow whoami # verify session (shows email, app, auth source)
36
- ```
32
+ securenow login # friendly flow: admin auth + app runtime connection
33
+ securenow admin login # admin/control-plane CLI and MCP auth only
34
+ securenow app connect # app/runtime SDK connection only
35
+ securenow admin login --token <JWT> # headless / CI admin auth
36
+ securenow whoami # verify both admin auth and runtime app status
37
+ ```
38
+
39
+ **Two-lane credentials (v7.8+):** admin/control-plane auth lives in `.securenow/admin.json`; SDK runtime app config and the firewall API key live in `.securenow/runtime.json`. `securenow login` can run both lanes for onboarding, but `securenow admin login` never replaces runtime app config and `securenow app connect` never replaces admin auth. Legacy combined `.securenow/credentials.json` files are still read.
37
40
 
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**.
41
+ **Zero-config runtime flow:** the browser step lets the user pick (or create) an app. The CLI stores the app's **key (UUID)** and **name** in `.securenow/runtime.json`. The SDK sends traces/logs to the default SecureNow ingestion gateway, which routes by app key, so no env vars or per-instance collector URLs are required for local dev or production.
39
42
 
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).
43
+ **Default-on security (v7.5.1+):** after picking or creating the app, `securenow app connect` turns on that app's firewall toggle, mints an API key with `firewall:read + blocklist:read + allowlist:read` scopes, and writes it into `.securenow/runtime.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 app connect, use `securenow api-key set snk_live_...` (see [API Key Management](#api-key-management) below).
41
44
 
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.
45
+ Runtime credentials resolve in order: project `.securenow/runtime.json` -> legacy project `.securenow/credentials.json` -> project named runtime credentials -> global `.securenow/runtime.json` -> legacy global credentials -> global named runtime credentials. Admin auth resolves from `admin.json` first, then legacy `credentials.json`. 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
46
 
44
- The **firewall API key** should live in the same credentials file as `apiKey`.
47
+ The **firewall API key** should live in runtime credentials as `apiKey`.
45
48
 
46
49
  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
50
 
@@ -62,11 +65,11 @@ node -r securenow/register src/index.js
62
65
  For Next.js, run the interactive scaffolding:
63
66
 
64
67
  ```bash
65
- securenow login
66
- securenow init
67
- ```
68
-
69
- This auto-detects your framework, creates the necessary `instrumentation.ts` and `next.config.js` changes, and reuses the app, instance, and firewall key written by login to `.securenow/credentials.json`.
68
+ securenow login
69
+ securenow init
70
+ ```
71
+
72
+ This auto-detects your framework, creates the necessary `instrumentation.ts` and `next.config.js` changes, and reuses the app, instance, and firewall key written by login or `app connect` to `.securenow/runtime.json`.
70
73
 
71
74
  ### Install This Skill in Cursor
72
75
 
@@ -76,16 +79,19 @@ Save this file as `.cursor/skills/securenow-cli/SKILL.md` in your project. Your
76
79
 
77
80
  Config lives in `~/.securenow/` (global) and optionally `.securenow/` (per-project):
78
81
 
79
- | File | Content |
80
- |------|---------|
81
- | `~/.securenow/config.json` | `apiUrl`, `appUrl`, `defaultApp`, `output` |
82
- | `~/.securenow/credentials.json` | `token`, `email`, `expiresAt`, `apiKey`, `app`, `config` (global, use `login --global`) |
83
- | `.securenow/credentials.json` | `token`, `email`, `expiresAt`, `apiKey`, `app`, `config`, `_securenow.explanations` (project-local default) |
82
+ | File | Content |
83
+ |------|---------|
84
+ | `~/.securenow/config.json` | `apiUrl`, `appUrl`, `defaultApp`, `output` |
85
+ | `~/.securenow/admin.json` | `token`, `email`, `expiresAt` for global admin/control-plane CLI and MCP auth |
86
+ | `~/.securenow/runtime.json` | `apiKey`, `app`, `config` for global SDK runtime |
87
+ | `.securenow/admin.json` | `token`, `email`, `expiresAt` for project-local admin/control-plane CLI and MCP auth |
88
+ | `.securenow/runtime.json` | `apiKey`, `app`, `config`, `_securenow.explanations` for project-local SDK runtime |
89
+ | `.securenow/credentials.json` | Legacy combined credentials; still read for backward compatibility |
84
90
  | `.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
91
 
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.
92
+ **Credential resolution order:** runtime config resolves from project `.securenow/runtime.json` -> legacy project `.securenow/credentials.json` -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> global `.securenow/runtime.json` -> legacy global credentials -> global named runtime credentials. Admin auth resolves from `admin.json` first, then legacy `credentials.json`. Legacy env fallbacks are disabled unless `SECURENOW_ENABLE_LEGACY_ENV=1` is set.
87
93
 
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.
94
+ **Firewall API key resolution (v7.8+):** project `.securenow/runtime.json` -> legacy project credentials -> project named runtime credentials in the fixed staging/production/preview/local/test/development/dev/prod order -> global runtime credentials -> legacy global credentials -> global named runtime credentials. Use `securenow app connect` for runtime setup or `securenow api-key set` to rotate a key without touching admin auth or env vars.
89
95
 
90
96
  ```bash
91
97
  securenow config set apiUrl https://api.securenow.ai
@@ -128,12 +134,13 @@ Spawns `node --require securenow/register [--import otel/hook.mjs] <script>`. ES
128
134
  ### Authentication
129
135
 
130
136
  ```bash
131
- securenow login # browser-based OAuth (stores in project ./.securenow/)
132
- securenow login --token <JWT> # headless / CI login
133
- securenow login --global # save credentials to ~/.securenow/ instead
134
- securenow logout # clear active credentials (local if present, else global)
135
- securenow logout --global # clear global credentials only
136
- securenow whoami # show email, user ID, API URL, auth source, expiry, default app
137
+ securenow login # friendly flow: admin auth + app runtime connection
138
+ securenow admin login # admin/control-plane CLI and MCP auth only
139
+ securenow admin login --token <JWT> # headless / CI admin auth
140
+ securenow app connect # app/runtime SDK connection only
141
+ securenow logout # clear admin auth; runtime app config remains
142
+ securenow logout --global # clear global admin auth only
143
+ securenow whoami # show admin auth and runtime app status separately
137
144
  ```
138
145
 
139
146
  ### Applications
@@ -151,7 +158,7 @@ securenow apps scan [--yes] # scan all app domains for new subd
151
158
 
152
159
  ### API Key Management
153
160
 
154
- Manage the firewall API key stored in the credentials file. Since v7.5.1 the login flow writes `snk_live_...` keys to `.securenow/credentials.json` by default, so no env var is required for local dev.
161
+ Manage the firewall API key stored in runtime credentials. Since v7.8 the app runtime flow writes `snk_live_...` keys to `.securenow/runtime.json` by default, so no env var is required for local dev.
155
162
 
156
163
  ```bash
157
164
  securenow api-key create --name "CLI firewall" # mint + save a firewall key with your logged-in session
@@ -163,7 +170,7 @@ securenow api-key clear --global # same, but from the global file
163
170
  securenow credentials runtime --env production # write .securenow/credentials.production.json for production secret-file deploys
164
171
  ```
165
172
 
166
- The key must start with `snk_live_`. `securenow login` with firewall enabled writes the key automatically; use `api-key create` when you have an account session but no runtime key yet, or `api-key set` when you already have a key from the dashboard.
173
+ The key must start with `snk_live_`. `securenow app connect` writes the key automatically; use `api-key create` when you have admin auth but no runtime key yet, or `api-key set` when you already have a key from the dashboard.
167
174
 
168
175
  ### Init — Project Setup
169
176
 
@@ -172,12 +179,12 @@ securenow init [--env local] [--key <API_KEY>]
172
179
  ```
173
180
 
174
181
  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 local credential JSON files are gitignored without ignoring the whole `.securenow/` directory
182
+ - **Credentials**: ensures `.securenow/runtime.json` exists, has secure defaults/explanations, and local credential JSON files are gitignored without ignoring the whole `.securenow/` directory
176
183
  - **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
184
  - **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
185
  - **Nuxt**: tells you to add `securenow/nuxt` to modules
179
186
  - **Node/Express/etc.**: suggests adding `-r securenow/register` to start script
180
- - Reuses `.securenow/credentials.json` written by login; production should mount/copy the tokenless runtime file generated by `securenow credentials runtime --env production`
187
+ - Reuses `.securenow/runtime.json` written by login/app connect; production should mount/copy the tokenless runtime file generated by `securenow credentials runtime --env production`
181
188
 
182
189
  ### Dashboard & Status
183
190
 
@@ -305,7 +312,7 @@ securenow firewall status --app <key> --env production # app/env toggle, sync
305
312
  securenow firewall test-ip <ip> --app <key> --env local # check if IP would be blocked
306
313
  ```
307
314
 
308
- **Zero-config setup (v7.5.1+):** running `securenow login` enables the selected app's firewall toggle, auto-mints an API key (scoped `firewall:read + blocklist:read + allowlist:read`), and writes it to the credentials file after the app is selected. No `SECURENOW_API_KEY` env var needed. If the user already has a key, `securenow api-key set snk_live_...` achieves the same thing. See [the landing firewall page](https://securenow.ai/firewall) for an overview.
315
+ **Zero-config setup:** running `securenow app connect` enables the selected app's firewall toggle, auto-mints an API key (scoped `firewall:read + blocklist:read + allowlist:read`), and writes it to `.securenow/runtime.json` after the app is selected. No `SECURENOW_API_KEY` env var needed. If the user already has a key, `securenow api-key set snk_live_...` achieves the same thing. See [the landing firewall page](https://securenow.ai/firewall) for an overview.
309
316
 
310
317
  ### Blocklist — Block Malicious IPs
311
318
 
@@ -313,12 +320,17 @@ securenow firewall test-ip <ip> --app <key> --env local # check if IP would be
313
320
  securenow blocklist # list blocked IPs
314
321
  securenow blocklist list
315
322
  securenow blocklist add <ip> --app <key> --env production --reason "Brute force"
323
+ securenow blocklist add <ip> --route /admin* --mode prefix --method ALL --reason "Admin probing"
316
324
  securenow blocklist unblock <id> --reason "False alarm after review"
317
325
  securenow blocklist remove <id> # compatibility alias for unblock
318
326
  securenow blocklist list --status removed # audit retained unblocks
319
327
  securenow blocklist stats # block counts, top reasons
320
328
  ```
321
329
 
330
+ Scoped blocks use the same model as rate limits: `--route`/`--path`/`--pattern`
331
+ sets `pathPattern`, `--mode`/`--path-mode` chooses `prefix`, `exact`, or
332
+ `regex`, and `--method` limits enforcement to one HTTP method or `ALL`.
333
+
322
334
  Unblock stops firewall enforcement but preserves the block report, history, and
323
335
  unblock audit fields. Reblocking the same IP later creates a fresh active block
324
336
  without erasing the previous investigation trail.
@@ -368,7 +380,13 @@ supporting evidence, not the primary automation score.
368
380
 
369
381
  ### Allowlist — Restrict to Known IPs
370
382
 
371
- ```bash
383
+ Dangerous production mode: IP Allowlist is deny-by-default. Once any active
384
+ allowlist entry exists for an app/environment, only listed IPs can reach it and
385
+ all other IPs are blocked. Do not use allowlist to mark an IP trusted, suppress
386
+ false positives, or clean up investigations. Use `securenow trusted add` for
387
+ known-safe monitors, office/VPN traffic, or trusted bypass cases.
388
+
389
+ ```bash
372
390
  securenow allowlist # list allowed IPs
373
391
  securenow allowlist list
374
392
  securenow allowlist add <ip> --app <key> --env local --label "Office" --reason "Corporate VPN"
@@ -376,10 +394,13 @@ securenow allowlist remove <id>
376
394
  securenow allowlist stats
377
395
  ```
378
396
 
379
- ### Trusted Proxies
397
+ ### Trusted IPs
380
398
 
381
- ```bash
382
- securenow trusted # list trusted IPs
399
+ Trusted IPs are the safe bypass/suppression mechanism for known infrastructure
400
+ and do not enable deny-by-default allowlist mode.
401
+
402
+ ```bash
403
+ securenow trusted # list trusted IPs
383
404
  securenow trusted list
384
405
  securenow trusted add <ip> [--label "CloudFlare edge"]
385
406
  securenow trusted remove <id>
@@ -473,7 +494,7 @@ securenow test-span
473
494
  securenow test-span "ci.smoke-test" # custom span name
474
495
  ```
475
496
 
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.
497
+ Both commands use the default SecureNow ingestion gateway plus optional advanced `config.otel.*` overrides from `.securenow/runtime.json` or legacy runtime credentials, and return non-zero on HTTP errors so CI/cron can detect failures.
477
498
 
478
499
  ### Utilities — Redaction, CIDR, Diagnostics
479
500
 
@@ -581,8 +602,8 @@ All commands support `--json` for structured output. When piping to other tools
581
602
 
582
603
  | Exit code / Error | Meaning | Recovery |
583
604
  |------------------|---------|----------|
584
- | `Session expired` | JWT expired | `securenow login` (or `login --global` for shared credentials) |
585
- | `Not logged in` | No token found | `securenow login` or mount/copy the right `.securenow/credentials.json` |
605
+ | `Session expired` | Admin JWT expired | `securenow admin login` (or `admin login --global` for shared admin auth) |
606
+ | `Not logged in` | No admin token found | `securenow admin login`; runtime `.securenow/runtime.json` is unrelated |
586
607
  | `Access denied (403)` | Insufficient plan or permissions | Upgrade plan or check user role |
587
608
  | `Cannot connect` | API unreachable | Check `securenow config get apiUrl` or network |
588
609
  | `Unknown command` | Typo or unrecognized command | `securenow help` |
package/app-config.js CHANGED
@@ -3,7 +3,8 @@
3
3
  /**
4
4
  * Shared SecureNow configuration resolver.
5
5
  *
6
- * Local development and production are driven by ./.securenow/credentials.json.
6
+ * Local development is driven by ./.securenow/runtime.json.
7
+ * Legacy combined ./.securenow/credentials.json files remain readable.
7
8
  * Named runtime files such as ./.securenow/credentials.staging.json and
8
9
  * ./.securenow/credentials.production.json are also accepted when the
9
10
  * canonical file is not present. Filename selection is deterministic and does
@@ -18,6 +19,7 @@ const os = require('os');
18
19
 
19
20
  const FREE_TRIAL_INSTANCE = 'https://ingest.securenow.ai';
20
21
  const DEFAULT_API_URL = 'https://api.securenow.ai';
22
+ const DEFAULT_FIREWALL_API_URL = FREE_TRIAL_INSTANCE;
21
23
  const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
22
24
  const LEGACY_ENV_FALLBACK_FLAG = 'SECURENOW_ENABLE_LEGACY_ENV';
23
25
  const CONFIG_SCHEMA_VERSION = 2;
@@ -31,6 +33,8 @@ const CREDENTIAL_FILE_ENVIRONMENTS = Object.freeze([
31
33
  'dev',
32
34
  'prod',
33
35
  ]);
36
+ const RUNTIME_CREDENTIALS_FILENAME = 'runtime.json';
37
+ const ADMIN_CREDENTIALS_FILENAME = 'admin.json';
34
38
 
35
39
  const DEFAULT_CONFIG = Object.freeze({
36
40
  logging: {
@@ -89,8 +93,8 @@ const DEFAULT_CONFIG = Object.freeze({
89
93
  });
90
94
 
91
95
  const CONFIG_EXPLANATIONS = Object.freeze({
92
- 'token': 'CLI session token written by `npx securenow login`. Secret: do not commit.',
93
- 'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by login or `securenow api-key set`. Secret: do not commit.',
96
+ 'token': 'Legacy CLI session token. New admin/control-plane auth is written to admin.json. Secret: do not commit.',
97
+ 'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by `securenow app connect` or `securenow api-key set`. Secret: do not commit.',
94
98
  'app.key': 'SecureNow application routing UUID. The SDK uses this as OTel service.name so dashboard queries match exactly.',
95
99
  'app.name': 'Human-readable app label shown in CLI output.',
96
100
  'app.routing': 'Telemetry uses the default SecureNow ingestion gateway and routes by app.key; runtime credentials do not expose per-instance collector URLs.',
@@ -110,8 +114,8 @@ const CONFIG_EXPLANATIONS = Object.freeze({
110
114
  'config.runtime.strict': 'If true, PM2/cluster workers exit when no app identity is resolvable.',
111
115
  'config.runtime.testSpan': 'If true, emit a startup smoke span. Prefer `npx securenow test-span` for manual checks.',
112
116
  'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
113
- 'config.firewall.enabled': 'Secure default: app firewall enforcement starts when apiKey is present and the dashboard toggle is on.',
114
- 'config.firewall.apiUrl': 'SecureNow API base URL for firewall sync.',
117
+ 'config.firewall.enabled': 'Deprecated local hint. Runtime firewall enforcement is controlled by the SecureNow dashboard/API app environment toggle; the SDK starts sync whenever apiKey is present.',
118
+ 'config.firewall.apiUrl': 'Optional SecureNow firewall control-plane base URL. Leave unset/default so hosted SDKs sync through the SecureNow ingest gateway.',
115
119
  'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version/ETag checks.',
116
120
  'config.firewall.syncInterval': 'Seconds between safety-net full firewall blocklist syncs.',
117
121
  'config.firewall.failMode': 'open allows traffic if SecureNow is temporarily unreachable; closed blocks all on sync failure.',
@@ -243,7 +247,7 @@ function uniq(values) {
243
247
  return out;
244
248
  }
245
249
 
246
- function credentialRelativePaths() {
250
+ function legacyCredentialRelativePaths() {
247
251
  return uniq([
248
252
  path.join('.securenow', 'credentials.json'),
249
253
  ...CREDENTIAL_FILE_ENVIRONMENTS.map((envName) =>
@@ -252,7 +256,25 @@ function credentialRelativePaths() {
252
256
  ]);
253
257
  }
254
258
 
255
- function resolveLocalCredentialsFile() {
259
+ function runtimeCredentialRelativePaths() {
260
+ return uniq([
261
+ path.join('.securenow', RUNTIME_CREDENTIALS_FILENAME),
262
+ ...legacyCredentialRelativePaths(),
263
+ ]);
264
+ }
265
+
266
+ function adminCredentialRelativePaths() {
267
+ return uniq([
268
+ path.join('.securenow', ADMIN_CREDENTIALS_FILENAME),
269
+ ...legacyCredentialRelativePaths(),
270
+ ]);
271
+ }
272
+
273
+ function credentialRelativePaths() {
274
+ return runtimeCredentialRelativePaths();
275
+ }
276
+
277
+ function resolveLocalFileFromRelativePaths(relativePaths) {
256
278
  const starts = [];
257
279
  try {
258
280
  if (typeof process !== 'undefined' && process.cwd) starts.push(process.cwd());
@@ -262,7 +284,7 @@ function resolveLocalCredentialsFile() {
262
284
  if (process.argv && process.argv[1]) starts.push(path.dirname(process.argv[1]));
263
285
  if (require.main && require.main.filename) starts.push(path.dirname(require.main.filename));
264
286
 
265
- const candidates = credentialRelativePaths();
287
+ const candidates = relativePaths;
266
288
  for (const start of uniq(starts)) {
267
289
  const found = findUpFirstFile(start, candidates);
268
290
  if (found) return found;
@@ -271,7 +293,15 @@ function resolveLocalCredentialsFile() {
271
293
  return null;
272
294
  }
273
295
 
274
- function resolveGlobalCredentialsFile() {
296
+ function resolveLocalCredentialsFile() {
297
+ return resolveLocalFileFromRelativePaths(runtimeCredentialRelativePaths());
298
+ }
299
+
300
+ function resolveLocalAdminCredentialsFile() {
301
+ return resolveLocalFileFromRelativePaths(adminCredentialRelativePaths());
302
+ }
303
+
304
+ function resolveGlobalFileFromRelativePaths(relativePaths) {
275
305
  let home;
276
306
  try {
277
307
  home = os.homedir();
@@ -279,7 +309,7 @@ function resolveGlobalCredentialsFile() {
279
309
  return null;
280
310
  }
281
311
 
282
- for (const relativePath of credentialRelativePaths()) {
312
+ for (const relativePath of relativePaths) {
283
313
  const found = fileIfReadable(path.join(home, relativePath));
284
314
  if (found) return found;
285
315
  }
@@ -287,6 +317,36 @@ function resolveGlobalCredentialsFile() {
287
317
  return null;
288
318
  }
289
319
 
320
+ function resolveGlobalCredentialsFile() {
321
+ return resolveGlobalFileFromRelativePaths(runtimeCredentialRelativePaths());
322
+ }
323
+
324
+ function resolveGlobalAdminCredentialsFile() {
325
+ return resolveGlobalFileFromRelativePaths(adminCredentialRelativePaths());
326
+ }
327
+
328
+ function runtimeCredentialsFromDocument(credentials) {
329
+ if (!credentials || typeof credentials !== 'object' || Array.isArray(credentials)) return null;
330
+ if (!credentials.runtime || typeof credentials.runtime !== 'object' || Array.isArray(credentials.runtime)) {
331
+ return credentials;
332
+ }
333
+
334
+ const runtime = clone(credentials.runtime);
335
+ if (runtime.environment) {
336
+ runtime.config = runtime.config || {};
337
+ runtime.config.runtime = runtime.config.runtime || {};
338
+ runtime.config.runtime.deploymentEnvironment = runtime.environment;
339
+ delete runtime.environment;
340
+ }
341
+ if (runtime.instanceUrl) {
342
+ runtime.config = runtime.config || {};
343
+ runtime.config.otel = runtime.config.otel || {};
344
+ if (!runtime.config.otel.endpoint) runtime.config.otel.endpoint = runtime.instanceUrl;
345
+ delete runtime.instanceUrl;
346
+ }
347
+ return runtime;
348
+ }
349
+
290
350
  function resolvePackageJsonFile() {
291
351
  const starts = [];
292
352
  try {
@@ -307,7 +367,7 @@ function resolvePackageJsonFile() {
307
367
 
308
368
  function loadLocalCredentials() {
309
369
  try {
310
- return withCredentialDefaults(readJsonSafe(resolveLocalCredentialsFile()));
370
+ return withCredentialDefaults(runtimeCredentialsFromDocument(readJsonSafe(resolveLocalCredentialsFile())));
311
371
  } catch {
312
372
  return null;
313
373
  }
@@ -315,7 +375,7 @@ function loadLocalCredentials() {
315
375
 
316
376
  function loadGlobalCredentials() {
317
377
  try {
318
- return withCredentialDefaults(readJsonSafe(resolveGlobalCredentialsFile()));
378
+ return withCredentialDefaults(runtimeCredentialsFromDocument(readJsonSafe(resolveGlobalCredentialsFile())));
319
379
  } catch {
320
380
  return null;
321
381
  }
@@ -325,7 +385,7 @@ function loadCredentials() {
325
385
  try {
326
386
  const localFile = resolveLocalCredentialsFile();
327
387
  if (localFile) {
328
- return withCredentialDefaults(readJsonSafe(localFile));
388
+ return withCredentialDefaults(runtimeCredentialsFromDocument(readJsonSafe(localFile)));
329
389
  }
330
390
  return loadGlobalCredentials();
331
391
  } catch {
@@ -457,6 +517,41 @@ function normalizeSignalEndpoint(value, signalType) {
457
517
  return endpoint;
458
518
  }
459
519
 
520
+ function normalizeFirewallApiUrl(value) {
521
+ const raw = pick(value);
522
+ if (raw == null) return DEFAULT_FIREWALL_API_URL;
523
+ const endpoint = normalizeInstanceEndpoint(raw);
524
+ if (!endpoint) return DEFAULT_FIREWALL_API_URL;
525
+ const trimmed = String(endpoint).trim().replace(/\/$/, '').replace(/\/api(?:\/v1)?$/i, '');
526
+
527
+ // api.securenow.ai was the historical SDK default. Hosted runtime sync now
528
+ // shares the ingest gateway so telemetry and firewall state fail or recover
529
+ // together, and so customer apps need only one public SecureNow egress host.
530
+ if (trimmed === DEFAULT_API_URL || trimmed === LEGACY_SECURENOW_GATEWAY) {
531
+ return DEFAULT_FIREWALL_API_URL;
532
+ }
533
+ if (trimmed === DEFAULT_FIREWALL_API_URL) return DEFAULT_FIREWALL_API_URL;
534
+
535
+ return trimmed;
536
+ }
537
+
538
+ function resolveFirewallApiUrl() {
539
+ return normalizeFirewallApiUrl(configValue('firewall.apiUrl', DEFAULT_API_URL));
540
+ }
541
+
542
+ function resolveFirewallApiFallbacks(primary = resolveFirewallApiUrl()) {
543
+ const normalizedPrimary = normalizeFirewallApiUrl(primary);
544
+ const fallbacks = [];
545
+
546
+ if (normalizedPrimary === DEFAULT_FIREWALL_API_URL) {
547
+ fallbacks.push(DEFAULT_API_URL);
548
+ } else if (normalizedPrimary === DEFAULT_API_URL) {
549
+ fallbacks.push(DEFAULT_FIREWALL_API_URL);
550
+ }
551
+
552
+ return uniq(fallbacks.filter((url) => url && url !== normalizedPrimary));
553
+ }
554
+
460
555
  function pick(value) {
461
556
  if (value === undefined || value === null) return null;
462
557
  if (typeof value === 'string') {
@@ -694,6 +789,10 @@ function resolveOtlpHeaders() {
694
789
  if (appKey && !headers['x-api-key']) {
695
790
  headers['x-api-key'] = appKey;
696
791
  }
792
+ const deploymentEnvironment = resolveDeploymentEnvironment();
793
+ if (deploymentEnvironment && !headers['x-securenow-environment']) {
794
+ headers['x-securenow-environment'] = deploymentEnvironment;
795
+ }
697
796
  return headers;
698
797
  }
699
798
 
@@ -714,19 +813,18 @@ function resolveEndpoints(options = {}) {
714
813
  }
715
814
 
716
815
  function resolveFirewallEnabled() {
717
- const fromConfig = pick(getPath(loadCredentials()?.config, 'firewall.enabled'));
718
- if (fromConfig != null) return parseBool(fromConfig, true);
719
-
720
- return parseBool(getPath(DEFAULT_CONFIG, 'firewall.enabled'), true);
816
+ return true;
721
817
  }
722
818
 
723
819
  function resolveFirewallOptions() {
820
+ const apiUrl = resolveFirewallApiUrl();
724
821
  return {
725
822
  apiKey: resolveApiKey(),
726
823
  appKey: resolveAppKey() || null,
727
824
  environment: resolveDeploymentEnvironment(),
728
825
  enabled: resolveFirewallEnabled(),
729
- apiUrl: configValue('firewall.apiUrl', DEFAULT_API_URL) || DEFAULT_API_URL,
826
+ apiUrl,
827
+ apiUrlFallbacks: resolveFirewallApiFallbacks(apiUrl),
730
828
  versionCheckInterval: numberConfig('firewall.versionCheckInterval', 10, 1),
731
829
  syncInterval: numberConfig('firewall.syncInterval', 3600, 1),
732
830
  failMode: configValue('firewall.failMode', 'open') || 'open',
@@ -755,10 +853,13 @@ function resolveFirewallOptions() {
755
853
  module.exports = {
756
854
  FREE_TRIAL_INSTANCE,
757
855
  DEFAULT_API_URL,
856
+ DEFAULT_FIREWALL_API_URL,
758
857
  LEGACY_SECURENOW_GATEWAY,
759
858
  LEGACY_ENV_FALLBACK_FLAG,
760
859
  CONFIG_SCHEMA_VERSION,
761
860
  CREDENTIAL_FILE_ENVIRONMENTS,
861
+ RUNTIME_CREDENTIALS_FILENAME,
862
+ ADMIN_CREDENTIALS_FILENAME,
762
863
  DEFAULT_CONFIG,
763
864
  CONFIG_EXPLANATIONS,
764
865
  ENV_TO_CONFIG_PATH,
@@ -783,6 +884,9 @@ module.exports = {
783
884
  resolveOtlpHeaderString,
784
885
  resolveEndpoints,
785
886
  resolveFirewallEnabled,
887
+ normalizeFirewallApiUrl,
888
+ resolveFirewallApiUrl,
889
+ resolveFirewallApiFallbacks,
786
890
  resolveFirewallOptions,
787
891
  env,
788
892
  boolEnv,
@@ -792,9 +896,14 @@ module.exports = {
792
896
  normalizeInstanceEndpoint,
793
897
  normalizeSignalEndpoint,
794
898
  headersToString,
899
+ legacyCredentialRelativePaths,
900
+ runtimeCredentialRelativePaths,
901
+ adminCredentialRelativePaths,
795
902
  credentialRelativePaths,
796
903
  resolveLocalCredentialsFile,
797
904
  resolveGlobalCredentialsFile,
905
+ resolveLocalAdminCredentialsFile,
906
+ resolveGlobalAdminCredentialsFile,
798
907
  loadCredentials,
799
908
  loadLocalCredentials,
800
909
  loadGlobalCredentials,
package/cli/apiKey.js CHANGED
@@ -34,15 +34,15 @@ async function create(args, flags) {
34
34
  name: result.apiKey?.name || name,
35
35
  preset,
36
36
  key: maskKey(key),
37
- savedTo: local ? 'project .securenow/credentials.json' : '~/.securenow/credentials.json',
37
+ savedTo: local ? 'project .securenow/runtime.json' : '~/.securenow/runtime.json',
38
38
  });
39
39
  return;
40
40
  }
41
41
 
42
42
  ui.success(`API key saved (${maskKey(key)})`);
43
43
  ui.info(local
44
- ? 'Stored in project .securenow/credentials.json (local)'
45
- : 'Stored in ~/.securenow/credentials.json (global)');
44
+ ? 'Stored in project .securenow/runtime.json (local)'
45
+ : 'Stored in ~/.securenow/runtime.json (global)');
46
46
  ui.info('The plaintext key is not printed. The SDK will read it from the credentials file.');
47
47
  } catch (err) {
48
48
  s.fail('Failed to create API key');
@@ -68,8 +68,8 @@ async function set(args, flags) {
68
68
 
69
69
  ui.success(`API key saved (${maskKey(key)})`);
70
70
  ui.info(local
71
- ? 'Stored in project .securenow/credentials.json (local)'
72
- : 'Stored in ~/.securenow/credentials.json (global)');
71
+ ? 'Stored in project .securenow/runtime.json (local)'
72
+ : 'Stored in ~/.securenow/runtime.json (global)');
73
73
  ui.info('The firewall will now pick it up automatically — no SECURENOW_API_KEY env var needed.');
74
74
  }
75
75
 
@@ -87,11 +87,11 @@ async function clear(args, flags) {
87
87
  async function show() {
88
88
  const key = config.getApiKey();
89
89
  if (!key) {
90
- ui.info('Runtime firewall enforcement key is missing. Run `npx securenow login` or `npx securenow api-key set snk_live_...` to refresh .securenow/credentials.json.');
90
+ ui.info('Runtime firewall enforcement key is missing. Run `npx securenow app connect` or `npx securenow api-key set snk_live_...` to refresh .securenow/runtime.json.');
91
91
  return;
92
92
  }
93
93
  console.log(maskKey(key));
94
- ui.info(`Source: ${config.getAuthSource()}`);
94
+ ui.info(`Source: ${config.getRuntimeSource()}`);
95
95
  }
96
96
 
97
97
  module.exports = { create, set, clear, show };
package/cli/apps.js CHANGED
@@ -66,7 +66,7 @@ async function list(args, flags) {
66
66
  if (!defaultApp && apps.length > 0) {
67
67
  console.log(` ${ui.c.bold('Use one of these apps in the current project:')}`);
68
68
  console.log(` ${ui.c.bold('securenow apps default <key>')}`);
69
- console.log(` ${ui.c.dim('(or run `securenow login` to pick interactively)')}`);
69
+ console.log(` ${ui.c.dim('(or run `securenow app connect` to pick interactively)')}`);
70
70
  console.log('');
71
71
  }
72
72
  } catch (err) {
@@ -127,7 +127,7 @@ async function create(args, flags) {
127
127
  console.log('');
128
128
  console.log(` ${ui.c.bold('Use this app in the current project:')}`);
129
129
  console.log(` ${ui.c.bold(`securenow apps default ${app.key}`)}`);
130
- console.log(` ${ui.c.dim('(writes .securenow/credentials.json no env var needed)')}`);
130
+ console.log(` ${ui.c.dim('(writes .securenow/runtime.json - no env var needed)')}`);
131
131
  console.log('');
132
132
 
133
133
  if (flags.json) {
@@ -285,7 +285,7 @@ async function setDefault(args) {
285
285
  }
286
286
  config.setConfigValue('defaultApp', key);
287
287
 
288
- // Mirror into credentials.json so the SDK picks this app up with zero env vars.
288
+ // Mirror into runtime credentials so the SDK picks this app up with zero env vars.
289
289
  try {
290
290
  requireAuth();
291
291
  const appData = await api.get(`/applications/${key}`);