securenow 7.2.1 → 7.4.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/cli/auth.js CHANGED
@@ -19,6 +19,18 @@ function openBrowser(url) {
19
19
  }
20
20
  }
21
21
 
22
+ function buildCliAuthUrl(appUrl, port, nonce, extra = {}) {
23
+ const url = new URL('/cli/auth', appUrl);
24
+ url.searchParams.set('callback', `http://127.0.0.1:${port}/callback`);
25
+ url.searchParams.set('state', nonce);
26
+ for (const [key, value] of Object.entries(extra)) {
27
+ if (value !== undefined && value !== null) {
28
+ url.searchParams.set(key, String(value));
29
+ }
30
+ }
31
+ return url.toString();
32
+ }
33
+
22
34
  function decodeJwtPayload(token) {
23
35
  try {
24
36
  const parts = token.split('.');
@@ -98,7 +110,7 @@ async function loginWithBrowser() {
98
110
  const email = payload?.email || 'unknown account';
99
111
  const safeEmail = email.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
100
112
  const port = server.address().port;
101
- const switchUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}&force_login=1`;
113
+ const switchUrl = buildCliAuthUrl(appUrl, port, nonce, { force_login: 1 });
102
114
 
103
115
  res.end([
104
116
  '<!DOCTYPE html><html><head><meta charset="utf-8"><title>SecureNow CLI Login</title></head>',
@@ -171,7 +183,7 @@ async function loginWithBrowser() {
171
183
 
172
184
  server.listen(0, '127.0.0.1', () => {
173
185
  const port = server.address().port;
174
- const authUrl = `${appUrl}/cli/auth?callback=http://127.0.0.1:${port}/callback&state=${encodeURIComponent(nonce)}`;
186
+ const authUrl = buildCliAuthUrl(appUrl, port, nonce);
175
187
 
176
188
  console.log('');
177
189
  ui.info('Opening browser for authentication...');
package/cli/firewall.js CHANGED
@@ -97,4 +97,78 @@ async function testIp(args, flags) {
97
97
  }
98
98
  }
99
99
 
100
- module.exports = { status, testIp };
100
+ async function resolveAppKey(flags) {
101
+ if (flags && flags.app) return String(flags.app);
102
+ try {
103
+ const { resolveAppKey: r } = require('../app-config');
104
+ const k = r();
105
+ if (k) return k;
106
+ } catch (_) {}
107
+ return null;
108
+ }
109
+
110
+ async function setEnabled(args, flags, enabled) {
111
+ requireAuth();
112
+ const appKey = await resolveAppKey(flags);
113
+ if (!appKey) {
114
+ ui.error('No app selected. Pass --app <key> or run `securenow login` / `securenow apps default <key>`.');
115
+ process.exit(1);
116
+ }
117
+
118
+ const verb = enabled ? 'Enabling' : 'Disabling';
119
+ const s = ui.spinner(`${verb} firewall for ${appKey}`);
120
+ try {
121
+ const data = await api.patch(`/firewall/app/${encodeURIComponent(appKey)}`, { enabled });
122
+ s.stop(`Firewall ${enabled ? 'enabled' : 'disabled'}`);
123
+
124
+ if (flags.json) { ui.json(data); return; }
125
+
126
+ const app = data.app || {};
127
+ console.log('');
128
+ console.log(` ${ui.c.bold(enabled ? ui.c.green('Firewall: ENABLED') : ui.c.yellow('Firewall: DISABLED'))} — ${app.name || appKey}`);
129
+ console.log(` ${ui.c.dim('App key:')} ${app.key || appKey}`);
130
+ console.log('');
131
+ console.log(` ${ui.c.dim('Running SDK instances pick up the change within ~10s.')}`);
132
+ console.log('');
133
+ } catch (err) {
134
+ s.fail(`Failed to ${enabled ? 'enable' : 'disable'} firewall`);
135
+ throw err;
136
+ }
137
+ }
138
+
139
+ async function enable(args, flags) { return setEnabled(args, flags, true); }
140
+ async function disable(args, flags) { return setEnabled(args, flags, false); }
141
+
142
+ async function appsList(args, flags) {
143
+ requireAuth();
144
+ const s = ui.spinner('Loading apps');
145
+ try {
146
+ const data = await api.get('/firewall/apps');
147
+ s.stop('Apps loaded');
148
+
149
+ if (flags.json) { ui.json(data); return; }
150
+
151
+ const apps = data.apps || [];
152
+ if (apps.length === 0) {
153
+ console.log('');
154
+ console.log(` ${ui.c.dim('No apps yet. Create one with `securenow apps create <name>`.')}`);
155
+ console.log('');
156
+ return;
157
+ }
158
+
159
+ console.log('');
160
+ for (const a of apps) {
161
+ const tag = a.firewallEnabled ? ui.c.green('ON ') : ui.c.yellow('OFF');
162
+ console.log(` ${tag} ${ui.c.bold(a.name)} ${ui.c.dim(a.key)}`);
163
+ }
164
+ console.log('');
165
+ console.log(` ${ui.c.dim('Toggle:')} securenow firewall enable --app <key>`);
166
+ console.log(` ${ui.c.dim(' :')} securenow firewall disable --app <key>`);
167
+ console.log('');
168
+ } catch (err) {
169
+ s.fail('Failed to list apps');
170
+ throw err;
171
+ }
172
+ }
173
+
174
+ module.exports = { status, testIp, enable, disable, appsList };
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');
@@ -178,10 +178,14 @@ const COMMANDS = {
178
178
  defaultSub: 'list',
179
179
  },
180
180
  firewall: {
181
- desc: 'Firewall status and IP testing',
181
+ desc: 'Firewall status, per-app toggle, and IP testing',
182
182
  usage: 'securenow firewall <subcommand> [options]',
183
+ flags: { app: 'App key (defaults to logged-in app)', json: 'Output as JSON' },
183
184
  sub: {
184
185
  status: { desc: 'Show firewall status, layers, and blocklist info', run: (a, f) => require('./cli/firewall').status(a, f) },
186
+ apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
187
+ enable: { desc: 'Turn the firewall ON for an app', usage: 'securenow firewall enable [--app <key>]', flags: { app: 'App key (defaults to logged-in app)' }, run: (a, f) => require('./cli/firewall').enable(a, f) },
188
+ disable: { desc: 'Turn the firewall OFF for an app', usage: 'securenow firewall disable [--app <key>]', flags: { app: 'App key (defaults to logged-in app)' }, run: (a, f) => require('./cli/firewall').disable(a, f) },
185
189
  'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip>', run: (a, f) => require('./cli/firewall').testIp(a, f) },
186
190
  },
187
191
  defaultSub: 'status',
@@ -25,7 +25,7 @@ Complete reference for all environment variables supported by SecureNow.
25
25
  | **SECURENOW_DISABLE_INSTRUMENTATIONS** | Optional | - | Comma-separated list of OTel instrumentations to disable |
26
26
  | **SECURENOW_TEST_SPAN** | Optional | `0` | Emit a single test span on startup (prefer `npx securenow test-span`) |
27
27
  | **SECURENOW_HIDE_BANNER** | Optional | `0` | Hide the free-trial banner |
28
- | **SECURENOW_FIREWALL_ENABLED** | Optional | `1` (on when API key is set) | Firewall master switch |
28
+ | **SECURENOW_FIREWALL_ENABLED** | Optional | (dashboard toggle) | Local override only — set to `0` to force-off regardless of dashboard. Primary control is the per-app toggle at `/dashboard/firewall` (≥ 7.3.0). |
29
29
  | **SECURENOW_ENABLE_MONGODB_INSTRUMENTATION** | Optional | `0` | Opt in to MongoDB instrumentation (off by default since a cursor bug on mongodb@6.6+; safe since SDK v6.0.2) |
30
30
  | **OTEL_SERVICE_NAME** | Optional | - | Alternative to SECURENOW_APPID (label only, no routing) |
31
31
  | **OTEL_EXPORTER_OTLP_ENDPOINT** | Optional | - | Alternative to SECURENOW_INSTANCE |
package/firewall-only.js CHANGED
@@ -16,12 +16,17 @@ 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 { resolveApiKey } = require('./app-config');
19
+ const { resolveApiKey, resolveAppKey } = require('./app-config');
20
20
  const firewallApiKey = resolveApiKey();
21
+ const appKey = resolveAppKey();
21
22
 
23
+ // SECURENOW_FIREWALL_ENABLED=0 is a hard local override (ops escape hatch).
24
+ // In all other cases the toggle lives in the dashboard; default ON when an
25
+ // API key is present.
22
26
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
23
27
  require('./firewall').init({
24
28
  apiKey: firewallApiKey,
29
+ appKey: appKey || null,
25
30
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
26
31
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
27
32
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
package/firewall.js CHANGED
@@ -16,9 +16,16 @@ let _initialized = false;
16
16
  let _consecutiveErrors = 0;
17
17
  let _layers = [];
18
18
  let _rawIps = [];
19
- let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
19
+ let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0, suppressedDisabled: 0 };
20
20
  let _localhostFallbackTried = false;
21
21
 
22
+ // Remote toggle — set by /firewall/sync when an appKey is in scope. Default
23
+ // true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
24
+ // When the dashboard / CLI flips this off, the next poll suppresses
25
+ // enforcement without restarting the host process.
26
+ let _remoteEnabled = true;
27
+ let _lastRemoteEnabled = null;
28
+
22
29
  // Allowlist state
23
30
  let _allowlistMatcher = null;
24
31
  let _allowlistRawIps = [];
@@ -133,7 +140,8 @@ function httpGet(url, extraHeaders, timeout, callback) {
133
140
  // ────── Unified Sync (v2 — single request for everything) ──────
134
141
 
135
142
  function doUnifiedSync(callback) {
136
- const url = buildUrl(_options.apiUrl, '/firewall/sync');
143
+ const appQuery = _options.appKey ? `?app=${encodeURIComponent(_options.appKey)}` : '';
144
+ const url = buildUrl(_options.apiUrl, '/firewall/sync') + appQuery;
137
145
  const headers = {};
138
146
  if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
139
147
  if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
@@ -165,6 +173,21 @@ function doUnifiedSync(callback) {
165
173
  let blChanged = false;
166
174
  let alChanged = false;
167
175
 
176
+ // Apply remote per-app toggle. Absent body.app means the backend either
177
+ // doesn't know about appKey-scoped sync (older API) or no appKey was
178
+ // sent — leave the previous value untouched (default true on first run).
179
+ if (body.app && typeof body.app.firewallEnabled === 'boolean') {
180
+ const next = body.app.firewallEnabled;
181
+ if (next !== _lastRemoteEnabled) {
182
+ _remoteEnabled = next;
183
+ if (_options.log) {
184
+ console.log('[securenow] Firewall: remote toggle → %s (app=%s)',
185
+ next ? 'ENABLED' : 'DISABLED', body.app.key || _options.appKey);
186
+ }
187
+ _lastRemoteEnabled = next;
188
+ }
189
+ }
190
+
168
191
  // Update blocklist version + data
169
192
  if (body.blocklist) {
170
193
  const newVer = body.blocklist.version;
@@ -559,6 +582,14 @@ function sendBlockResponse(req, res, ip) {
559
582
  }
560
583
 
561
584
  function firewallRequestHandler(req, res) {
585
+ // Remote disable wins over everything: when the dashboard / CLI flips the
586
+ // toggle off, requests pass through the SDK as if the firewall weren't
587
+ // installed. The poll loop keeps running so we re-enable within seconds.
588
+ if (_remoteEnabled === false) {
589
+ _stats.suppressedDisabled++;
590
+ return false;
591
+ }
592
+
562
593
  const ip = resolveClientIp(req);
563
594
 
564
595
  // Allowlist check: if active, only listed IPs are allowed through
@@ -711,10 +742,16 @@ function getStats() {
711
742
  circuitState: _circuitState,
712
743
  consecutiveErrors: _consecutiveErrors,
713
744
  unifiedSync: _useUnifiedSync,
745
+ remoteEnabled: _remoteEnabled,
746
+ appKey: _options ? _options.appKey || null : null,
714
747
  };
715
748
  }
716
749
 
717
- function getMatcher() { return _matcher; }
718
- function getAllowlistMatcher() { return _allowlistMatcher; }
750
+ // Layers (TCP / iptables / cloud) read the matcher to populate kernel-level
751
+ // rules. When the remote toggle is off, return null so they treat the policy
752
+ // as "no IPs to block" without us mutating the cached matcher.
753
+ function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
754
+ function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
755
+ function isRemoteEnabled() { return _remoteEnabled !== false; }
719
756
 
720
- module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher };
757
+ module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, isRemoteEnabled };
package/nextjs.js CHANGED
@@ -616,11 +616,14 @@ function registerSecureNow(options = {}) {
616
616
  // Firewall — runs independently from OTel so it works even if tracing fails.
617
617
  // Key comes from env OR .securenow/credentials.json (set via
618
618
  // `npx securenow api-key set snk_live_...`), so you don't need a .env entry.
619
- const firewallApiKey = require('./app-config').resolveApiKey();
619
+ const appConfig = require('./app-config');
620
+ const firewallApiKey = appConfig.resolveApiKey();
621
+ const firewallAppKey = appConfig.resolveAppKey();
620
622
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
621
623
  try {
622
624
  require('./firewall').init({
623
625
  apiKey: firewallApiKey,
626
+ appKey: firewallAppKey || null,
624
627
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
625
628
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
626
629
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
@@ -328,11 +328,13 @@ export default defineNitroPlugin((nitroApp) => {
328
328
 
329
329
  // ── Firewall — runs independently from OTel ──
330
330
  const firewallApiKey = env('SECURENOW_API_KEY');
331
+ const firewallAppKey = env('SECURENOW_APPID') || null;
331
332
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
332
333
  try {
333
334
  const { init: fwInit } = await import('./firewall.js');
334
335
  fwInit({
335
336
  apiKey: firewallApiKey,
337
+ appKey: firewallAppKey,
336
338
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
337
339
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
338
340
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.2.1",
3
+ "version": "7.4.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",
package/tracing.js CHANGED
@@ -611,11 +611,15 @@ const sdk = new NodeSDK({
611
611
  patchHttpForBanner();
612
612
  }
613
613
 
614
- // Firewall — auto-activates when SECURENOW_API_KEY is set
615
- const firewallApiKey = env('SECURENOW_API_KEY');
614
+ // Firewall — auto-activates only when a real snk_live_ key is resolvable.
615
+ // resolveApiKey() enforces the prefix, so we skip cleanly when the app has
616
+ // only an app-routing UUID (or nothing at all) — no 401 polling loops.
617
+ const firewallApiKey = appConfig.resolveApiKey();
618
+ const firewallAppKey = appConfig.resolveAppKey();
616
619
  if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
617
620
  require('./firewall').init({
618
621
  apiKey: firewallApiKey,
622
+ appKey: firewallAppKey || null,
619
623
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
620
624
  versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
621
625
  syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,