securenow 7.0.0-anas.3 → 7.2.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.
package/NPM_README.md CHANGED
@@ -59,17 +59,25 @@ yarn add securenow
59
59
 
60
60
  ### 1. Automatic Setup (Recommended)
61
61
 
62
- Run the init command after installing:
62
+ Run login it's a browser flow that picks an app and, since v7.1.0, also offers one-click firewall onboarding:
63
63
 
64
64
  ```bash
65
- npx securenow init --key snk_live_abc123...
65
+ npx securenow login
66
+ ```
67
+
68
+ During the browser step you can choose **Enable the Firewall?** — if you accept, the dashboard mints an API key (scoped `firewall:read + blocklist:read + allowlist:read`) and the CLI writes it into `.securenow/credentials.json`. No env vars, no copy-pasting keys.
69
+
70
+ For framework scaffolding (Next.js `instrumentation.ts`, etc.) use:
71
+
72
+ ```bash
73
+ npx securenow init --key snk_live_abc123... # --key is optional now that login handles it
66
74
  ```
67
75
 
68
76
  This detects your framework and:
69
77
  - **Next.js**: Creates `instrumentation.ts`, suggests `withSecureNow()` for `next.config.js`
70
78
  - **Nuxt 3**: Suggests adding `securenow/nuxt` to modules
71
79
  - **Express / Node.js**: Shows how to add `-r securenow/register` to your start script
72
- - **All**: Writes `SECURENOW_API_KEY` to `.env.local` when `--key` is provided
80
+ - **All**: Writes `SECURENOW_API_KEY` to `.env.local` when `--key` is provided (not needed if `login` already wrote it to `.securenow/credentials.json`)
73
81
 
74
82
  ### 2. Manual Setup
75
83
 
@@ -144,7 +152,10 @@ The `securenow` CLI gives you full access to the SecureNow platform from the ter
144
152
  ### Getting Started
145
153
 
146
154
  ```bash
147
- # Log in (opens browser for OAuth)
155
+ # Log in (opens browser for OAuth + app picker)
156
+ # Since v7.1.0 the browser flow also offers one-click firewall onboarding —
157
+ # accept "Enable the Firewall?" to have the CLI mint and store the API key
158
+ # in .securenow/credentials.json automatically (no env var needed).
148
159
  npx securenow login
149
160
 
150
161
  # Or use a token for CI/headless environments
@@ -155,6 +166,11 @@ npx securenow login --local
155
166
 
156
167
  # Check who you're logged in as (shows auth source)
157
168
  npx securenow whoami
169
+
170
+ # Already have a firewall key? Store it without re-running login:
171
+ npx securenow api-key set snk_live_abc123... # --global for ~/.securenow/
172
+ npx securenow api-key show # masked key + source
173
+ npx securenow api-key clear # remove just the key
158
174
  ```
159
175
 
160
176
  ### Project Setup
@@ -461,6 +477,9 @@ npx securenow logs --json --level error | jq '.logs'
461
477
  | | `apps info <id>` | Application details |
462
478
  | | `apps delete <id>` | Delete application |
463
479
  | | `apps default <key>` | Set default app |
480
+ | **API Key** | `api-key set <snk_live_...> [--global]` | Save firewall key to `.securenow/credentials.json` |
481
+ | | `api-key show` | Show masked key + source |
482
+ | | `api-key clear [--global]` | Remove stored key (leaves session/app) |
464
483
  | **Observe** | `traces` | List traces |
465
484
  | | `traces show <id>` | Trace details |
466
485
  | | `traces analyze <id>` | AI trace analysis |
@@ -1076,16 +1095,28 @@ The Nuxt server plugin (v5.13.0+) initializes the firewall independently from Op
1076
1095
 
1077
1096
  ## Firewall -- Automatic IP Blocking
1078
1097
 
1079
- SecureNow can automatically block IPs from your blocklist at the application layer. No code changes -- just set an API key and the firewall activates.
1098
+ SecureNow can automatically block IPs from your blocklist at the application layer. No code changes -- just provide an API key (via `securenow login`, the `api-key` CLI, or env var) and the firewall activates.
1080
1099
 
1081
1100
  ### Enable the Firewall
1082
1101
 
1102
+ Pick whichever fits your environment:
1103
+
1083
1104
  ```bash
1084
- # Add to your .env
1105
+ # (a) Zero-config (v7.1+): login + opt in to the firewall in the browser.
1106
+ # Key is minted and written to .securenow/credentials.json automatically.
1107
+ npx securenow login
1108
+
1109
+ # (b) Already have a key? Write it to the creds file directly:
1110
+ npx securenow api-key set snk_live_abc123...
1111
+
1112
+ # (c) Old-school env var (still works; preferred for CI/Docker/prod):
1113
+ # .env
1085
1114
  SECURENOW_API_KEY=snk_live_abc123...
1086
1115
  ```
1087
1116
 
1088
- That's it. On startup, you'll see:
1117
+ The SDK resolves the firewall key in this order: `SECURENOW_API_KEY` env var (only if it starts with `snk_live_`) → project `./.securenow/credentials.json` global `~/.securenow/credentials.json`.
1118
+
1119
+ On startup, you'll see:
1089
1120
 
1090
1121
  ```
1091
1122
  [securenow] Firewall: ENABLED
@@ -1231,7 +1262,7 @@ See the [Firewall Guide](./docs/FIREWALL-GUIDE.md) for the full reference.
1231
1262
 
1232
1263
  | Variable | Description | Default |
1233
1264
  |----------|-------------|---------|
1234
- | `SECURENOW_API_KEY` | API key with `firewall:read` scope. Enables the firewall when set. | - |
1265
+ | `SECURENOW_API_KEY` | API key with `firewall:read` scope. Enables the firewall when set. Since v7.1.0 the firewall also reads this from `.securenow/credentials.json` (written by `securenow login` or `securenow api-key set`); env var only wins if it starts with `snk_live_`. | from creds file |
1235
1266
  | `SECURENOW_API_URL` | SecureNow API base URL. Auto-detected for co-located deployments (falls back to `http://localhost:4000` on ECONNREFUSED). | `https://api.securenow.ai` |
1236
1267
  | `SECURENOW_FIREWALL_ENABLED` | Master kill-switch. Set to `0` to disable. | `1` |
1237
1268
  | `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between version checks (lightweight ETag-based). | `10` |
package/README.md CHANGED
@@ -12,7 +12,9 @@ Zero-config OpenTelemetry for Node.js, Next.js, and Nuxt — traces, logs, body
12
12
  # 1. Install
13
13
  npm install securenow
14
14
 
15
- # 2. Pick (or create) your app in the browser — writes .securenow/ locally
15
+ # 2. Pick (or create) your app in the browser — writes .securenow/ locally.
16
+ # Since v7.1.0, the browser step also offers one-click firewall onboarding:
17
+ # say yes and the CLI stores a scoped API key in the same file — no env vars.
16
18
  npx securenow login
17
19
 
18
20
  # 3. Start your app — one flag is all it takes
@@ -135,10 +137,11 @@ Resolution order (first non-empty wins):
135
137
 
136
138
  ```bash
137
139
  # Setup
138
- npx securenow login # browser auth + app picker (saves to ./.securenow/)
140
+ npx securenow login # browser auth + app picker + firewall onboarding (saves to ./.securenow/)
139
141
  npx securenow login --global # save to ~/.securenow/ instead
140
142
  npx securenow login --token <TOKEN> # headless (CI)
141
143
  npx securenow init # scaffold Next.js instrumentation files
144
+ npx securenow api-key set snk_live_... # store firewall key in .securenow/credentials.json
142
145
 
143
146
  # Apps
144
147
  npx securenow apps # list all apps
@@ -241,12 +244,15 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
241
244
 
242
245
  | Command | Description |
243
246
  |---|---|
244
- | `securenow login` | Browser auth + pick app (writes ./.securenow/ by default) |
247
+ | `securenow login` | Browser auth + pick app + optional firewall key onboarding (writes ./.securenow/ by default) |
245
248
  | `securenow login --global` | Save to ~/.securenow/ instead |
246
249
  | `securenow login --token <TOKEN>` | Headless (CI/servers) |
247
250
  | `securenow logout` | Clear project-local credentials |
248
251
  | `securenow logout --global` | Clear ~/.securenow/ instead |
249
252
  | `securenow whoami` | Show current session (email, app, expiry) |
253
+ | `securenow api-key set <snk_live_...>` | Store firewall key in `.securenow/credentials.json` (`--global` for `~/.securenow/`) |
254
+ | `securenow api-key show` | Print masked key + source file |
255
+ | `securenow api-key clear` | Remove stored key (`--global` for `~/.securenow/`) |
250
256
 
251
257
  ### Applications
252
258
 
package/SKILL-API.md CHANGED
@@ -48,12 +48,16 @@ That's it. Traces and logs flow to your OTLP collector. No code changes for Expr
48
48
 
49
49
  ### 3. Enable the Firewall (Optional)
50
50
 
51
- Add one more env var to auto-activate IP blocking:
51
+ Since v7.1.0 the firewall key lives in your credentials file — no env var required:
52
52
 
53
53
  ```bash
54
- SECURENOW_API_KEY=snk_live_abc123...
54
+ npx securenow login # pick app + click "Enable firewall" in browser
55
+ # or, if you already have one:
56
+ npx securenow api-key set snk_live_abc123...
55
57
  ```
56
58
 
59
+ Both paths write the key to `.securenow/credentials.json` (auto-gitignored) and the firewall activates on next start. Setting `SECURENOW_API_KEY=snk_live_...` in the environment still works and takes precedence.
60
+
57
61
  The firewall syncs your blocklist and enforces it on every request — zero code changes.
58
62
 
59
63
  ---
@@ -259,7 +263,7 @@ Instruments document load, fetch, XMLHttpRequest, and user interactions with bro
259
263
 
260
264
  ## Firewall — Multi-Layer IP Blocking
261
265
 
262
- The firewall auto-activates when `SECURENOW_API_KEY` is set. It syncs your blocklist from the SecureNow API and enforces it across up to four layers:
266
+ The firewall auto-activates once an API key is resolvable. Since **v7.1.0** the key is read from `.securenow/credentials.json` (written by `npx securenow login` or `securenow api-key set`), so the `SECURENOW_API_KEY` env var is optional. Resolution order: env (must start with `snk_live_`) → project `./.securenow/credentials.json` → global `~/.securenow/credentials.json`.
263
267
 
264
268
  ```
265
269
  Layer 4: Cloud/Edge WAF → blocked at CDN (Cloudflare, AWS WAF, GCP Cloud Armor)
@@ -271,11 +275,16 @@ Layer 1: HTTP Handler → 403 JSON response (always active)
271
275
  ### Activate
272
276
 
273
277
  ```bash
274
- # .env
275
- SECURENOW_API_KEY=snk_live_abc123... # auto-activates Layer 1
278
+ # Zero-config (recommended) — writes the key to .securenow/credentials.json
279
+ npx securenow login # pick app + click "Enable firewall"
280
+ # or, if you already have a key:
281
+ npx securenow api-key set snk_live_abc123...
282
+
283
+ # Optional .env overrides for the stronger layers
276
284
  SECURENOW_FIREWALL_TCP=1 # opt-in Layer 2
277
285
  SECURENOW_FIREWALL_IPTABLES=1 # opt-in Layer 3 (Linux, needs root)
278
286
  SECURENOW_FIREWALL_CLOUD=cloudflare # opt-in Layer 4
287
+ # SECURENOW_API_KEY=snk_live_... # still honored; only wins if it starts with snk_live_
279
288
  ```
280
289
 
281
290
  ### Firewall-Only Mode (No Tracing Overhead)
@@ -483,7 +492,7 @@ securenow redact @request.json --fields internal_id,sessionHash
483
492
 
484
493
  | Variable | Description | Default |
485
494
  |----------|-------------|---------|
486
- | `SECURENOW_API_KEY` | API key (`snk_live_...`); activates firewall when set | — |
495
+ | `SECURENOW_API_KEY` | API key (`snk_live_...`); activates firewall when set. Since v7.1.0, this is also read from `.securenow/credentials.json` — env var only wins when it starts with `snk_live_`. | — |
487
496
  | `SECURENOW_API_URL` | SecureNow API base URL | `https://api.securenow.ai` |
488
497
  | `SECURENOW_FIREWALL_ENABLED` | Master kill-switch (`0` to disable) | `1` |
489
498
  | `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight version checks | `10` |
@@ -542,16 +551,16 @@ No code changes to the application needed.
542
551
 
543
552
  ```bash
544
553
  npm install securenow
545
- npx securenow init --key snk_live_abc123...
554
+ npx securenow login # pick app + "Enable firewall" in browser
546
555
  ```
547
556
 
548
- The `init` command creates `instrumentation.ts` and suggests `next.config` changes. Then set env vars in `.env.local`:
557
+ `securenow login` writes session, app, and firewall key to `.securenow/credentials.json` (auto-gitignored). The `init` command still works for manual setup — it creates `instrumentation.ts` and suggests `next.config` changes. Most users only need this `.env.local`:
549
558
 
550
559
  ```
551
560
  SECURENOW_APPID=my-nextjs-app
552
561
  SECURENOW_INSTANCE=https://your-collector:4318
553
- SECURENOW_API_KEY=snk_live_abc123...
554
562
  SECURENOW_CAPTURE_BODY=1
563
+ # SECURENOW_API_KEY=snk_live_... (otherwise lives in .securenow/credentials.json)
555
564
  ```
556
565
 
557
566
  ### Enable Firewall With Zero Tracing Overhead
@@ -562,7 +571,7 @@ For apps that only need IP blocking:
562
571
  node -r securenow/firewall-only app.js
563
572
  ```
564
573
 
565
- Set `SECURENOW_API_KEY` in the environment. No other configuration needed.
574
+ Make sure an API key is resolvable — either run `npx securenow login` / `securenow api-key set snk_live_...` (writes the creds file), or set `SECURENOW_API_KEY` in the environment. No other configuration needed.
566
575
 
567
576
  ### Production Hardened Configuration
568
577
 
package/SKILL-CLI.md CHANGED
@@ -26,9 +26,13 @@ securenow whoami # verify session (shows email, app, auth source)
26
26
 
27
27
  **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**.
28
28
 
29
+ **Firewall onboarding (v7.1+):** after picking the app, `securenow login` asks "Enable the Firewall?" in the browser. If you accept, the dashboard mints an API key with `firewall:read + blocklist:read + allowlist:read` scopes and the CLI writes it into `.securenow/credentials.json` automatically — no `SECURENOW_API_KEY` env var 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).
30
+
29
31
  Credentials resolve in order: `SECURENOW_TOKEN` env var → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`.
30
32
 
31
- For CI / Docker / production, set env vars directly (always win over the file): `SECURENOW_APPID=<uuid>`, `SECURENOW_INSTANCE=<url>`, `SECURENOW_API_KEY=<uuid>`.
33
+ The **firewall API key** resolves in a slightly different order: `SECURENOW_API_KEY` env var (only if it starts with `snk_live_`) → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`. An env var that isn't the `snk_live_` format is ignored, so the file can still win.
34
+
35
+ For CI / Docker / production, set env vars directly (always win over the file): `SECURENOW_APPID=<uuid>`, `SECURENOW_INSTANCE=<url>`, `SECURENOW_API_KEY=snk_live_<...>`.
32
36
 
33
37
  ### Integrate With Your App
34
38
 
@@ -66,6 +70,8 @@ Config lives in `~/.securenow/` (global) and optionally `.securenow/` (per-proje
66
70
 
67
71
  **Credential resolution order:** `SECURENOW_TOKEN` env var → `.securenow/credentials.json` (project) → `~/.securenow/credentials.json` (global).
68
72
 
73
+ **Firewall API key resolution (v7.1+):** `SECURENOW_API_KEY` env var (only honored if it starts with `snk_live_`) → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`. Use `securenow api-key set` to write the key to the credentials file without touching env vars.
74
+
69
75
  ```bash
70
76
  securenow config set apiUrl https://api.securenow.ai
71
77
  securenow config set defaultApp my-app-key
@@ -128,6 +134,20 @@ securenow apps discover [appId] [--domain example.com] # discover subdomains, a
128
134
  securenow apps scan [--yes] # scan all app domains for new subdomains
129
135
  ```
130
136
 
137
+ ### API Key Management
138
+
139
+ Manage the firewall API key stored in the credentials file. Since v7.1.0 the firewall reads `snk_live_...` keys from `.securenow/credentials.json` so no env var is required.
140
+
141
+ ```bash
142
+ securenow api-key set snk_live_xxxxxxxxxx # save to project ./.securenow/ (default)
143
+ securenow api-key set snk_live_xxx --global # save to ~/.securenow/ instead
144
+ securenow api-key show # print the masked current key + its source
145
+ securenow api-key clear # remove just the API key (keeps session/app)
146
+ securenow api-key clear --global # same, but from the global file
147
+ ```
148
+
149
+ The key must start with `snk_live_`. `securenow login` with firewall enabled writes the key automatically; use `api-key set` when you already have a key from the dashboard, or to rotate it later.
150
+
131
151
  ### Init — Project Setup
132
152
 
133
153
  ```bash
@@ -225,6 +245,8 @@ securenow firewall status # layers, sync time, blocked count,
225
245
  securenow firewall test-ip <ip> # check if IP would be blocked
226
246
  ```
227
247
 
248
+ **Zero-config setup (v7.1+):** running `securenow login` and opting into "Enable the Firewall?" in the browser auto-mints an API key (scoped `firewall:read + blocklist:read + allowlist:read`) and writes it to the credentials file. 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.
249
+
228
250
  ### Blocklist — Block Malicious IPs
229
251
 
230
252
  ```bash
package/app-config.js CHANGED
@@ -14,6 +14,10 @@
14
14
  * - What human label to show? (resolveAppName) — display only, never
15
15
  * sent to the collector.
16
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.
17
21
  *
18
22
  * Resolution order (first non-empty wins):
19
23
  *
@@ -25,7 +29,7 @@
25
29
  * 5. Hard default / null
26
30
  *
27
31
  * Credentials file schema:
28
- * { token, email, expiresAt, app: { key: <uuid>, name: <display>, instance } }
32
+ * { token, email, expiresAt, apiKey: <snk_live_...>, app: { key, name, instance } }
29
33
  */
30
34
 
31
35
  const fs = require('fs');
@@ -112,6 +116,21 @@ function resolveAppId() {
112
116
  return resolveAppKey() || resolveAppName();
113
117
  }
114
118
 
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
+ function resolveApiKey() {
124
+ const fromEnv = pick(process.env.SECURENOW_API_KEY);
125
+ if (fromEnv && fromEnv.startsWith('snk_live_')) return fromEnv;
126
+
127
+ const creds = loadCredentials();
128
+ const fromCreds = creds && pick(creds.apiKey);
129
+ if (fromCreds && fromCreds.startsWith('snk_live_')) return fromCreds;
130
+
131
+ return null;
132
+ }
133
+
115
134
  function resolveInstance() {
116
135
  const fromEnv =
117
136
  pick(process.env.SECURENOW_INSTANCE) ||
@@ -143,6 +162,7 @@ module.exports = {
143
162
  resolveAppKey,
144
163
  resolveAppName,
145
164
  resolveAppId,
165
+ resolveApiKey,
146
166
  resolveInstance,
147
167
  resolveAll,
148
168
  loadCredentials,
package/cli/apiKey.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const config = require('./config');
4
+ const ui = require('./ui');
5
+
6
+ function maskKey(key) {
7
+ if (!key || key.length < 16) return key || '';
8
+ return `${key.slice(0, 12)}••••••${key.slice(-4)}`;
9
+ }
10
+
11
+ async function set(args, flags) {
12
+ const key = args[0];
13
+ if (!key) {
14
+ ui.error('API key is required. Usage: securenow api-key set <snk_live_...>');
15
+ process.exit(1);
16
+ }
17
+ if (!key.startsWith('snk_live_')) {
18
+ ui.error('API key must start with "snk_live_"');
19
+ ui.info('Create one in the dashboard: Settings → API Keys');
20
+ process.exit(1);
21
+ }
22
+
23
+ const local = flags.global ? false : true;
24
+ config.setApiKey(key, { local });
25
+ if (local) config.ensureLocalGitignore();
26
+
27
+ ui.success(`API key saved (${maskKey(key)})`);
28
+ ui.info(local
29
+ ? 'Stored in project .securenow/credentials.json (local)'
30
+ : 'Stored in ~/.securenow/credentials.json (global)');
31
+ ui.info('The firewall will now pick it up automatically — no SECURENOW_API_KEY env var needed.');
32
+ }
33
+
34
+ async function clear(args, flags) {
35
+ const local = flags.global ? false : true;
36
+ const existing = config.getApiKey();
37
+ config.clearApiKey({ local });
38
+ if (existing) {
39
+ ui.success(`Cleared API key (${maskKey(existing)})`);
40
+ } else {
41
+ ui.info('No API key was stored');
42
+ }
43
+ }
44
+
45
+ async function show() {
46
+ const key = config.getApiKey();
47
+ if (!key) {
48
+ ui.info('No API key stored in credentials. Falling back to SECURENOW_API_KEY env var.');
49
+ return;
50
+ }
51
+ console.log(maskKey(key));
52
+ ui.info(`Source: ${config.getAuthSource()}`);
53
+ }
54
+
55
+ module.exports = { set, clear, show };
package/cli/auth.js CHANGED
@@ -45,6 +45,14 @@ async function loginWithBrowser() {
45
45
  return new Promise((resolve, reject) => {
46
46
  let pendingToken = null;
47
47
  let pendingApp = null;
48
+ let pendingApiKey = null;
49
+ const sockets = new Set();
50
+
51
+ const closeServer = () => {
52
+ try { server.close(); } catch {}
53
+ for (const s of sockets) { try { s.destroy(); } catch {} }
54
+ sockets.clear();
55
+ };
48
56
 
49
57
  const server = http.createServer((req, res) => {
50
58
  const url = new URL(req.url, `http://127.0.0.1`);
@@ -56,19 +64,20 @@ async function loginWithBrowser() {
56
64
  const appKey = url.searchParams.get('app_key');
57
65
  const appName = url.searchParams.get('app_name');
58
66
  const appInstance = url.searchParams.get('app_instance');
67
+ const apiKey = url.searchParams.get('api_key');
59
68
 
60
69
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
61
70
 
62
71
  if (error) {
63
72
  res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Authentication Failed</h2><p>You can close this window.</p></body></html>');
64
- server.close();
73
+ closeServer();
65
74
  reject(new CLIError(`Authentication failed: ${error}`));
66
75
  return;
67
76
  }
68
77
 
69
78
  if (returnedState !== nonce) {
70
79
  res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Security Error</h2><p>State mismatch — this request may not have originated from your CLI. Please try again.</p></body></html>');
71
- server.close();
80
+ closeServer();
72
81
  reject(new CLIError('State mismatch on callback — possible CSRF. Please retry `securenow login`.'));
73
82
  return;
74
83
  }
@@ -82,6 +91,9 @@ async function loginWithBrowser() {
82
91
  instance: appInstance || null,
83
92
  };
84
93
  }
94
+ if (apiKey && apiKey.startsWith('snk_live_')) {
95
+ pendingApiKey = apiKey;
96
+ }
85
97
  const payload = decodeJwtPayload(token);
86
98
  const email = payload?.email || 'unknown account';
87
99
  const safeEmail = email.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -124,7 +136,7 @@ async function loginWithBrowser() {
124
136
  }
125
137
 
126
138
  res.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Something went wrong</h2><p>No token received. Please try again.</p></body></html>');
127
- server.close();
139
+ closeServer();
128
140
  reject(new CLIError('No token received in callback'));
129
141
  return;
130
142
  }
@@ -139,10 +151,12 @@ async function loginWithBrowser() {
139
151
  res.end('{"ok":true}');
140
152
  const token = pendingToken;
141
153
  const app = pendingApp;
154
+ const apiKeyFromLogin = pendingApiKey;
142
155
  pendingToken = null;
143
156
  pendingApp = null;
144
- server.close();
145
- resolve({ token, app });
157
+ pendingApiKey = null;
158
+ closeServer();
159
+ resolve({ token, app, apiKey: apiKeyFromLogin });
146
160
  return;
147
161
  }
148
162
 
@@ -150,6 +164,11 @@ async function loginWithBrowser() {
150
164
  res.end();
151
165
  });
152
166
 
167
+ server.on('connection', (socket) => {
168
+ sockets.add(socket);
169
+ socket.once('close', () => sockets.delete(socket));
170
+ });
171
+
153
172
  server.listen(0, '127.0.0.1', () => {
154
173
  const port = server.address().port;
155
174
  const authUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}`;
@@ -170,7 +189,7 @@ async function loginWithBrowser() {
170
189
  console.log(ui.c.dim(' Waiting for authentication...'));
171
190
 
172
191
  const timeout = setTimeout(() => {
173
- server.close();
192
+ closeServer();
174
193
  reject(new CLIError('Login timed out after 5 minutes. Try `securenow login --token <TOKEN>` instead.'));
175
194
  }, 5 * 60 * 1000);
176
195
 
@@ -219,12 +238,13 @@ async function login(args, flags) {
219
238
  }
220
239
 
221
240
  try {
222
- const { token, app } = await loginWithBrowser();
241
+ const { token, app, apiKey } = await loginWithBrowser();
223
242
  const payload = decodeJwtPayload(token);
224
243
  const email = payload?.email || 'unknown';
225
244
  const exp = payload?.exp ? payload.exp * 1000 : null;
226
245
 
227
246
  config.setAuth(token, email, exp, { local, app });
247
+ if (apiKey) config.setApiKey(apiKey, { local });
228
248
  if (local) config.ensureLocalGitignore();
229
249
  console.log('');
230
250
  ui.success(`Logged in as ${ui.c.bold(email)}`);
@@ -232,6 +252,9 @@ async function login(args, flags) {
232
252
  if (app && (app.name || app.key)) {
233
253
  ui.info(`Linked to app ${ui.c.bold(app.name || app.key)}${app.key ? ` (${ui.c.dim(app.key)})` : ''}`);
234
254
  }
255
+ if (apiKey) {
256
+ ui.info(`Firewall API key saved — the firewall will activate automatically on next start`);
257
+ }
235
258
  if (exp) {
236
259
  const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
237
260
  ui.info(`Session expires in ${days} days`);
package/cli/config.js CHANGED
@@ -135,6 +135,27 @@ function getApp() {
135
135
  return creds && creds.app ? creds.app : null;
136
136
  }
137
137
 
138
+ function setApiKey(apiKey, { local } = {}) {
139
+ const useLocal = local === true || (local == null && hasLocalCredentials());
140
+ const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
141
+ const existing = loadJSON(targetFile);
142
+ saveJSON(targetFile, { ...existing, apiKey });
143
+ }
144
+
145
+ function clearApiKey({ local } = {}) {
146
+ const useLocal = local === true || (local == null && hasLocalCredentials());
147
+ const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
148
+ const existing = loadJSON(targetFile);
149
+ if (!existing || !existing.apiKey) return;
150
+ delete existing.apiKey;
151
+ saveJSON(targetFile, existing);
152
+ }
153
+
154
+ function getApiKey() {
155
+ const creds = loadCredentials();
156
+ return creds && creds.apiKey ? creds.apiKey : null;
157
+ }
158
+
138
159
  function setApp(app, { local } = {}) {
139
160
  const useLocal = local === true || (local == null && hasLocalCredentials());
140
161
  const targetFile = useLocal ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
@@ -193,6 +214,9 @@ module.exports = {
193
214
  setAuth,
194
215
  getApp,
195
216
  setApp,
217
+ setApiKey,
218
+ clearApiKey,
219
+ getApiKey,
196
220
  getAuthSource,
197
221
  hasLocalCredentials,
198
222
  ensureLocalGitignore,
@@ -25,7 +25,9 @@ function resolvedConfig() {
25
25
  ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/logs`
26
26
  : `${instance.replace(/\/$/, '')}/v1/logs`);
27
27
  const headers = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
28
- const apiKey = process.env.SECURENOW_API_KEY || '';
28
+ // Resolve firewall API key the same way the SDK does: env, then
29
+ // .securenow/credentials.json (project-local, then global).
30
+ const apiKey = require('../app-config').resolveApiKey() || '';
29
31
  const apiUrl = config.getApiUrl();
30
32
  const loggingEnabled = !/^(0|false)$/i.test(String(process.env.SECURENOW_LOGGING_ENABLED ?? ''));
31
33
  const captureBody = !/^(0|false)$/i.test(String(process.env.SECURENOW_CAPTURE_BODY ?? ''));
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
4
  const ui = require('./cli/ui');
@@ -73,6 +73,30 @@ const COMMANDS = {
73
73
  usage: 'securenow whoami',
74
74
  run: () => require('./cli/auth').whoami(),
75
75
  },
76
+ 'api-key': {
77
+ desc: 'Manage the firewall API key stored in .securenow/credentials.json',
78
+ usage: 'securenow api-key <subcommand> [options]',
79
+ sub: {
80
+ set: {
81
+ desc: 'Save an API key (snk_live_...) to the credentials file',
82
+ usage: 'securenow api-key set <snk_live_...> [--global]',
83
+ flags: { global: 'Save to ~/.securenow/ instead of project-local' },
84
+ run: (a, f) => require('./cli/apiKey').set(a, f),
85
+ },
86
+ clear: {
87
+ desc: 'Remove the stored API key',
88
+ usage: 'securenow api-key clear [--global]',
89
+ flags: { global: 'Clear from ~/.securenow/ instead of project-local' },
90
+ run: (a, f) => require('./cli/apiKey').clear(a, f),
91
+ },
92
+ show: {
93
+ desc: 'Print the masked API key currently in use',
94
+ usage: 'securenow api-key show',
95
+ run: () => require('./cli/apiKey').show(),
96
+ },
97
+ },
98
+ defaultSub: 'show',
99
+ },
76
100
  apps: {
77
101
  desc: 'Manage applications',
78
102
  usage: 'securenow apps <subcommand> [options]',
@@ -41,6 +41,43 @@ The `snk_live_` prefix makes it easy to identify SecureNow keys in your codebase
41
41
 
42
42
  ---
43
43
 
44
+ ## Storing the API Key
45
+
46
+ Since v7.1.0, the firewall reads its API key from `.securenow/credentials.json` as well as `SECURENOW_API_KEY`. You don't need to manage env vars for local dev.
47
+
48
+ **Resolution order (firewall):**
49
+
50
+ 1. `SECURENOW_API_KEY` env var — **only if it starts with `snk_live_`** (non-matching values are ignored so the file can still win)
51
+ 2. Project `./.securenow/credentials.json`
52
+ 3. Global `~/.securenow/credentials.json`
53
+
54
+ ### Writing the key to the credentials file
55
+
56
+ ```bash
57
+ # Interactive onboarding — picks/creates an app and, if you opt in,
58
+ # mints a key scoped firewall:read + blocklist:read + allowlist:read
59
+ # (the "firewall" preset, used by default for CLI firewall onboarding)
60
+ # and writes it to ./.securenow/credentials.json automatically.
61
+ npx securenow login
62
+
63
+ # Already have a key? Write it directly:
64
+ npx securenow api-key set snk_live_abc123...
65
+
66
+ # Save to ~/.securenow/ instead of the project
67
+ npx securenow api-key set snk_live_abc123... --global
68
+
69
+ # Inspect (masked) and see which file it came from
70
+ npx securenow api-key show
71
+
72
+ # Remove just the API key (leaves session/app in place)
73
+ npx securenow api-key clear
74
+ npx securenow api-key clear --global
75
+ ```
76
+
77
+ The key must start with `snk_live_` — `api-key set` rejects anything else.
78
+
79
+ ---
80
+
44
81
  ## Scopes (Permissions)
45
82
 
46
83
  Each API key has a set of scopes that control what it can access. Scopes follow the `resource:action` pattern.
@@ -117,13 +154,21 @@ curl -s https://api.securenow.ai/api/v1/blocklist \
117
154
 
118
155
  ### In the Firewall SDK
119
156
 
120
- Set the `SECURENOW_API_KEY` environment variable:
157
+ Easiest path (v7.1+) — let the CLI write the key to the credentials file for you:
158
+
159
+ ```bash
160
+ npx securenow login # firewall onboarding in the browser
161
+ # or, if you already have a key:
162
+ npx securenow api-key set snk_live_abc...
163
+ ```
164
+
165
+ Or set the env var the old way:
121
166
 
122
167
  ```bash
123
168
  SECURENOW_API_KEY=snk_live_abc123...
124
169
  ```
125
170
 
126
- The firewall SDK reads this automatically on startup.
171
+ The firewall SDK reads the credentials file on startup if the env var is unset (or isn't a `snk_live_` key). Env vars still take precedence when present and well-formed — useful in CI / Docker / prod.
127
172
 
128
173
  ### In CI/CD
129
174
 
@@ -533,6 +533,8 @@ export NODE_ENV=test
533
533
  export SECURENOW_API_KEY=snk_live_a1b2c3d4e5f6...
534
534
  ```
535
535
 
536
+ **v7.1.0+:** the firewall also reads this key from `.securenow/credentials.json` (written by `securenow login` with firewall enabled, or by `securenow api-key set`). The env var only wins if it starts with `snk_live_` — otherwise the credentials file is used, so you can rely on the file for local dev without unsetting any stray env var. Setting an app UUID here (the old pre-7.1 habit) is ignored for firewall auth and would produce silent 401s; always use a `snk_live_...` key.
537
+
536
538
  ---
537
539
 
538
540
  ### SECURENOW_API_URL
@@ -44,17 +44,26 @@ All layers share the same in-memory blocklist, synced from the SecureNow API usi
44
44
 
45
45
  ### 1. Get an API Key
46
46
 
47
+ Two ways to get the firewall wired up — pick whichever fits:
48
+
47
49
  ```bash
48
- # Log in to SecureNow
50
+ # (a) Zero-config (v7.1+): run login and choose "Enable firewall" in the browser.
51
+ # The dashboard mints a key scoped firewall:read + blocklist:read + allowlist:read
52
+ # and the CLI writes it to .securenow/credentials.json. No further config needed.
49
53
  npx securenow login
50
54
 
51
- # View your firewall status and API key
55
+ # (b) Already have a key? Drop it into the credentials file directly:
56
+ npx securenow api-key set snk_live_abc123...
57
+
58
+ # View your firewall status and API key source
52
59
  npx securenow firewall status
53
60
  ```
54
61
 
55
62
  Or create an API key from the dashboard: **Settings → API Keys → Create Key** with the `firewall:read` scope.
56
63
 
57
- ### 2. Add the Key to Your Environment
64
+ ### 2. Add the Key to Your Environment (optional)
65
+
66
+ If you used `securenow login` or `securenow api-key set` above, you can **skip this step** — the SDK reads the key from `.securenow/credentials.json` at boot. Env vars are still supported for CI / Docker / prod:
58
67
 
59
68
  ```bash
60
69
  # .env
@@ -67,7 +76,7 @@ SECURENOW_API_KEY=snk_live_abc123...
67
76
  node -r securenow/register app.js
68
77
  ```
69
78
 
70
- That's it. The firewall auto-activates when `SECURENOW_API_KEY` is present. You'll see:
79
+ That's it. The firewall auto-activates as soon as a valid `snk_live_...` key is available (env var or credentials file). You'll see:
71
80
 
72
81
  ```
73
82
  [securenow] Firewall: ENABLED
@@ -191,7 +200,7 @@ SECURENOW_FIREWALL_CLOUD_DRY_RUN=1
191
200
 
192
201
  | Variable | Default | Description |
193
202
  |----------|---------|-------------|
194
- | `SECURENOW_API_KEY` | *(required)* | API key with `firewall:read` scope |
203
+ | `SECURENOW_API_KEY` | *(from creds file)* | API key with `firewall:read` scope. Since v7.1.0 the firewall also reads this from `.securenow/credentials.json` (written by `securenow login` or `securenow api-key set`), so this env var is optional. The env var only wins if it starts with `snk_live_`; otherwise the credentials file is used. |
195
204
  | `SECURENOW_API_URL` | `https://api.securenow.ai` | API base URL. Auto-fallback to `http://localhost:4000` on ECONNREFUSED. |
196
205
  | `SECURENOW_FIREWALL_ENABLED` | `1` | Master kill-switch (`0` to disable) |
197
206
  | `SECURENOW_FIREWALL_VERSION_INTERVAL` | `10` | Seconds between version checks (lightweight ETag-based) |
package/docs/INDEX.md CHANGED
@@ -80,8 +80,8 @@ Complete documentation for SecureNow - OpenTelemetry instrumentation for Node.js
80
80
 
81
81
  ### 🛡️ Security & Protection
82
82
 
83
- - **[Firewall Guide](FIREWALL-GUIDE.md)** - Automatic IP blocking (multi-layer: HTTP, TCP, iptables, Cloud WAF)
84
- - **[API Keys Guide](API-KEYS-GUIDE.md)** - Creating, managing, and securing API keys
83
+ - **[Firewall Guide](FIREWALL-GUIDE.md)** - Automatic IP blocking (multi-layer: HTTP, TCP, iptables, Cloud WAF). v7.1+: `securenow login` now offers one-click firewall onboarding that writes the key to `.securenow/credentials.json`.
84
+ - **[API Keys Guide](API-KEYS-GUIDE.md)** - Creating, managing, and securing API keys. Includes the `securenow api-key set|show|clear` command family (v7.1+).
85
85
  - **[Redaction Examples](REDACTION-EXAMPLES.md)** - How sensitive data is redacted
86
86
  - **[Automatic IP Capture](AUTOMATIC-IP-CAPTURE.md)** - IP address and metadata collection
87
87
 
package/firewall-only.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * NODE_OPTIONS='-r securenow/firewall-only' next start
9
9
  *
10
10
  * Reads .env via dotenv (if installed), then initialises the HTTP-level
11
- * firewall when SECURENOW_API_KEY is present.
11
+ * firewall when an API key is resolvable (env var or .securenow/credentials.json).
12
12
  */
13
13
 
14
14
  try { require('dotenv').config(); } catch (_) {}
@@ -16,7 +16,8 @@ try { require('dotenv').config(); } catch (_) {}
16
16
  const env = (k) =>
17
17
  process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
18
18
 
19
- const firewallApiKey = env('SECURENOW_API_KEY');
19
+ const { resolveApiKey } = require('./app-config');
20
+ const firewallApiKey = resolveApiKey();
20
21
 
21
22
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
22
23
  require('./firewall').init({
package/nextjs.js CHANGED
@@ -610,8 +610,10 @@ function registerSecureNow(options = {}) {
610
610
  }
611
611
  }
612
612
 
613
- // Firewall — runs independently from OTel so it works even if tracing fails
614
- const firewallApiKey = env('SECURENOW_API_KEY');
613
+ // Firewall — runs independently from OTel so it works even if tracing fails.
614
+ // Key comes from env OR .securenow/credentials.json (set via
615
+ // `npx securenow api-key set snk_live_...`), so you don't need a .env entry.
616
+ const firewallApiKey = require('./app-config').resolveApiKey();
615
617
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
616
618
  try {
617
619
  require('./firewall').init({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.0.0-anas.3",
3
+ "version": "7.2.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
@@ -165,7 +165,8 @@
165
165
  },
166
166
  "overrides": {
167
167
  "@opentelemetry/api": "1.7.0",
168
- "@opentelemetry/api-logs": "0.47.0"
168
+ "@opentelemetry/api-logs": "0.47.0",
169
+ "protobufjs": "^7.5.5"
169
170
  },
170
171
  "sideEffects": true,
171
172
  "license": "ISC"