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/NPM_README.md +35 -22
- package/README.md +50 -32
- package/SKILL-API.md +49 -25
- package/SKILL-CLI.md +61 -40
- package/app-config.js +127 -18
- package/cli/apiKey.js +7 -7
- package/cli/apps.js +3 -3
- package/cli/auth.js +113 -31
- package/cli/client.js +14 -13
- package/cli/config.js +219 -45
- package/cli/credentials.js +3 -3
- package/cli/diagnostics.js +35 -10
- package/cli/firewall.js +19 -7
- package/cli/init.js +5 -5
- package/cli/security.js +31 -11
- package/cli.js +57 -22
- package/firewall-only.js +4 -4
- package/firewall.js +172 -45
- package/mcp/catalog.js +43 -30
- package/mcp/server.js +73 -12
- package/nextjs.js +49 -11
- package/nuxt-server-plugin.mjs +8 -4
- package/otel-defaults.js +11 -0
- package/package.json +2 -1
- package/tracing.js +49 -12
- package/web-vite.mjs +3 -0
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
|
|
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 #
|
|
33
|
-
securenow login
|
|
34
|
-
securenow
|
|
35
|
-
securenow
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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/
|
|
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/
|
|
83
|
-
|
|
|
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/
|
|
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.
|
|
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 #
|
|
132
|
-
securenow login
|
|
133
|
-
securenow login --
|
|
134
|
-
securenow
|
|
135
|
-
securenow logout
|
|
136
|
-
securenow
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
397
|
+
### Trusted IPs
|
|
380
398
|
|
|
381
|
-
|
|
382
|
-
|
|
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/
|
|
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
|
|
585
|
-
| `Not logged in` | No token found | `securenow login
|
|
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
|
|
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
|
|
93
|
-
'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by
|
|
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': '
|
|
114
|
-
'config.firewall.apiUrl': 'SecureNow
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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/
|
|
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/
|
|
45
|
-
: 'Stored in ~/.securenow/
|
|
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/
|
|
72
|
-
: 'Stored in ~/.securenow/
|
|
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
|
|
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.
|
|
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
|
|
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/
|
|
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
|
|
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}`);
|