securenow 5.12.1 → 5.14.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/firewall.js CHANGED
@@ -23,10 +23,17 @@ async function status(args, flags) {
23
23
  ui.keyValue([
24
24
  ['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
25
25
  ['Last updated', data.updatedAt || 'unknown'],
26
+ ['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
27
+ ['Allowlist updated', data.allowlistUpdatedAt || 'never'],
26
28
  ['Sync TTL', `${data.ttl || 60}s`],
27
29
  ]);
30
+ if (data.allowlistCount > 0) {
31
+ console.log('');
32
+ console.log(` ${ui.c.yellow('!')} Allowlist is active — only ${data.allowlistCount} IP(s) can reach your app`);
33
+ }
28
34
  console.log('');
29
35
  console.log(` ${ui.c.dim('Manage your blocklist:')} securenow blocklist`);
36
+ console.log(` ${ui.c.dim('Manage your allowlist:')} securenow allowlist`);
30
37
  console.log(` ${ui.c.dim('Test an IP:')} securenow firewall test-ip <ip>`);
31
38
  console.log('');
32
39
  } catch (err) {
@@ -63,14 +70,26 @@ async function testIp(args, flags) {
63
70
 
64
71
  console.log('');
65
72
  if (data.blocked) {
66
- console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is in the blocklist`);
67
- if (data.matchedEntry && data.matchedEntry !== ip) {
68
- console.log(` ${ui.c.dim(`Matched by: ${data.matchedEntry}`)}`);
73
+ if (data.allowlistActive && !data.allowlisted) {
74
+ console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} ${ip} is not on the allowlist`);
75
+ console.log(` ${ui.c.dim('Allowlist is active — only listed IPs are permitted')}`);
76
+ } else {
77
+ console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is in the blocklist`);
78
+ if (data.matchedEntry && data.matchedEntry !== ip) {
79
+ console.log(` ${ui.c.dim(`Matched by: ${data.matchedEntry}`)}`);
80
+ }
69
81
  }
70
82
  } else {
71
- console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is not in the blocklist`);
83
+ if (data.allowlisted) {
84
+ console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is on the allowlist`);
85
+ } else {
86
+ console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is not in the blocklist`);
87
+ }
72
88
  }
73
89
  console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
90
+ if (data.allowlistActive) {
91
+ console.log(` ${ui.c.dim('Allowlist is active')}`);
92
+ }
74
93
  console.log('');
75
94
  } catch (err) {
76
95
  s.fail('Failed to test IP');
package/cli/init.js CHANGED
@@ -4,97 +4,198 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const ui = require('./ui');
6
6
 
7
- const templates = {
8
- typescript: `import { registerSecureNow } from 'securenow/nextjs';
7
+ const INSTRUMENTATION_JS = `export async function register() {
8
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
9
+ const { registerSecureNow } = require('securenow/nextjs');
10
+ registerSecureNow();
11
+ }
12
+ }
13
+ `;
14
+
15
+ const INSTRUMENTATION_TS = `export async function register() {
16
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
17
+ const { registerSecureNow } = require('securenow/nextjs');
18
+ registerSecureNow();
19
+ }
20
+ }
21
+ `;
22
+
23
+ function detectProject(dir) {
24
+ const pkgPath = path.join(dir, 'package.json');
25
+ if (!fs.existsSync(pkgPath)) return { framework: 'unknown' };
9
26
 
10
- export function register() {
11
- registerSecureNow();
27
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
28
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
29
+
30
+ if (allDeps.next) return { framework: 'nextjs', pkg, pkgPath, nextVersion: allDeps.next };
31
+ if (allDeps.nuxt) return { framework: 'nuxt', pkg, pkgPath };
32
+ if (allDeps.express) return { framework: 'express', pkg, pkgPath };
33
+ if (allDeps.fastify) return { framework: 'fastify', pkg, pkgPath };
34
+ if (allDeps.koa) return { framework: 'koa', pkg, pkgPath };
35
+ if (allDeps.hapi || allDeps['@hapi/hapi']) return { framework: 'hapi', pkg, pkgPath };
36
+ return { framework: 'node', pkg, pkgPath };
12
37
  }
13
- `,
14
- javascript: `const { registerSecureNow } = require('securenow/nextjs');
15
38
 
16
- export function register() {
17
- registerSecureNow();
39
+ function findInstrumentationFile(dir) {
40
+ for (const name of ['instrumentation.ts', 'instrumentation.js', 'src/instrumentation.ts', 'src/instrumentation.js']) {
41
+ const p = path.join(dir, name);
42
+ if (fs.existsSync(p)) return p;
43
+ }
44
+ return null;
18
45
  }
19
- `,
20
- env: `# SecureNow Configuration
21
- SECURENOW_APPID=my-nextjs-app
22
- SECURENOW_INSTANCE=http://your-otlp-backend:4318
23
- # OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-api-key-here"
24
- # OTEL_LOG_LEVEL=info
25
- `,
26
- };
27
-
28
- function isNextJsProject(dir) {
29
- try {
30
- const pkgPath = path.join(dir, 'package.json');
31
- if (!fs.existsSync(pkgPath)) return false;
32
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
33
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
34
- return !!deps.next;
35
- } catch {
36
- return false;
46
+
47
+ function findNextConfig(dir) {
48
+ for (const name of ['next.config.js', 'next.config.mjs', 'next.config.ts']) {
49
+ const p = path.join(dir, name);
50
+ if (fs.existsSync(p)) return p;
37
51
  }
52
+ return null;
38
53
  }
39
54
 
40
- async function init(args, flags) {
41
- const cwd = process.cwd();
55
+ function hasTypeScript(dir) {
56
+ return fs.existsSync(path.join(dir, 'tsconfig.json'));
57
+ }
42
58
 
43
- console.log('');
44
- ui.heading('SecureNow Setup');
45
- console.log('');
59
+ async function init(_args, flags) {
60
+ const dir = process.cwd();
61
+ const project = detectProject(dir);
46
62
 
47
- const isNext = isNextJsProject(cwd);
48
- if (!isNext && !flags.force) {
49
- ui.warn("This doesn't appear to be a Next.js project.");
50
- ui.info('Use --force to proceed anyway.');
63
+ ui.header('SecureNow Project Setup');
64
+
65
+ if (project.framework === 'unknown') {
66
+ ui.error('No package.json found. Run this command in your project root.');
51
67
  process.exit(1);
52
68
  }
53
69
 
54
- const useTS = flags.typescript || flags.ts ||
55
- (!flags.javascript && !flags.js && fs.existsSync(path.join(cwd, 'tsconfig.json')));
56
- const useSrc = flags.src ||
57
- (!flags.root && fs.existsSync(path.join(cwd, 'src')));
70
+ ui.info(`Detected framework: ${ui.bold(project.framework)}`);
71
+
72
+ if (project.framework === 'nextjs') {
73
+ await initNextJs(dir, project, flags);
74
+ } else if (project.framework === 'nuxt') {
75
+ initNuxt(dir, project);
76
+ } else {
77
+ initNode(dir, project);
78
+ }
58
79
 
59
- const fileName = useTS ? 'instrumentation.ts' : 'instrumentation.js';
60
- const filePath = useSrc ? path.join(cwd, 'src', fileName) : path.join(cwd, fileName);
80
+ initEnv(dir, flags);
61
81
 
62
- if (fs.existsSync(filePath) && !flags.force) {
63
- ui.error(`${useSrc ? 'src/' : ''}${fileName} already exists. Use --force to overwrite.`);
64
- process.exit(1);
82
+ console.log('');
83
+ ui.success('Setup complete! Run your app to verify.');
84
+ }
85
+
86
+ async function initNextJs(dir, project, flags) {
87
+ const useTs = hasTypeScript(dir);
88
+ const ext = useTs ? 'ts' : 'js';
89
+
90
+ const existing = findInstrumentationFile(dir);
91
+ if (existing) {
92
+ ui.info(`instrumentation file already exists: ${path.relative(dir, existing)}`);
93
+ } else {
94
+ const filePath = path.join(dir, `instrumentation.${ext}`);
95
+ const content = useTs ? INSTRUMENTATION_TS : INSTRUMENTATION_JS;
96
+ fs.writeFileSync(filePath, content, 'utf8');
97
+ ui.success(`Created instrumentation.${ext}`);
65
98
  }
66
99
 
67
- try {
68
- const template = useTS ? templates.typescript : templates.javascript;
69
- if (useSrc) fs.mkdirSync(path.join(cwd, 'src'), { recursive: true });
70
- fs.writeFileSync(filePath, template, 'utf8');
71
- ui.success(`Created ${useSrc ? 'src/' : ''}${fileName}`);
72
- } catch (err) {
73
- ui.error(`Failed to create instrumentation file: ${err.message}`);
74
- process.exit(1);
100
+ const configPath = findNextConfig(dir);
101
+ if (configPath) {
102
+ const content = fs.readFileSync(configPath, 'utf8');
103
+ if (content.includes('withSecureNow')) {
104
+ ui.info('next.config already uses withSecureNow skipping');
105
+ } else if (content.includes('serverExternalPackages') && content.includes('securenow')) {
106
+ ui.info('next.config already externalizes securenow skipping');
107
+ } else if (content.includes('serverComponentsExternalPackages') && content.includes('securenow')) {
108
+ ui.info('next.config already externalizes securenow — skipping');
109
+ } else {
110
+ ui.warn(`Update your ${path.basename(configPath)} to use withSecureNow():`);
111
+ console.log('');
112
+ console.log(` const { withSecureNow } = require('securenow/nextjs-webpack-config');`);
113
+ console.log(` module.exports = withSecureNow({ /* your config */ });`);
114
+ console.log('');
115
+ }
116
+ } else {
117
+ const newConfigPath = path.join(dir, 'next.config.js');
118
+ fs.writeFileSync(newConfigPath, `const { withSecureNow } = require('securenow/nextjs-webpack-config');\n\nmodule.exports = withSecureNow({\n reactStrictMode: true,\n});\n`, 'utf8');
119
+ ui.success('Created next.config.js with withSecureNow()');
75
120
  }
121
+ }
76
122
 
77
- const envPath = path.join(cwd, '.env.local');
78
- if (!fs.existsSync(envPath) || flags.force) {
79
- try {
80
- fs.writeFileSync(envPath, templates.env, 'utf8');
81
- ui.success('Created .env.local template');
82
- } catch (err) {
83
- ui.warn(`Could not create .env.local: ${err.message}`);
123
+ function initNuxt(dir, project) {
124
+ const configPath = path.join(dir, 'nuxt.config.ts');
125
+ if (fs.existsSync(configPath)) {
126
+ const content = fs.readFileSync(configPath, 'utf8');
127
+ if (content.includes('securenow/nuxt')) {
128
+ ui.info('nuxt.config already references securenow/nuxt — skipping');
129
+ } else {
130
+ ui.warn('Add securenow/nuxt to your nuxt.config modules:');
131
+ console.log('');
132
+ console.log(" modules: ['securenow/nuxt'],");
133
+ console.log('');
84
134
  }
85
135
  } else {
86
- ui.info('.env.local already exists (skipped)');
136
+ ui.warn('Add securenow/nuxt to your nuxt.config modules array.');
87
137
  }
138
+ }
88
139
 
89
- console.log('');
90
- console.log(` ${ui.c.bold('Next steps:')}`);
91
- console.log('');
92
- console.log(` 1. Edit ${ui.c.cyan('.env.local')} and set your SECURENOW_APPID`);
93
- console.log(` 2. Run ${ui.c.cyan('npm run dev')} to start your app`);
94
- console.log(` 3. Check traces in the SecureNow dashboard`);
95
- console.log('');
96
- ui.info(`Don't have an app key yet? Run ${ui.c.bold('securenow apps create <name>')}`);
97
- console.log('');
140
+ function initNode(dir, project) {
141
+ const pkg = project.pkg;
142
+ const scripts = pkg.scripts || {};
143
+
144
+ const startScript = scripts.start || '';
145
+ if (startScript.includes('securenow/register')) {
146
+ ui.info('start script already uses securenow/register — skipping');
147
+ return;
148
+ }
149
+
150
+ if (startScript) {
151
+ ui.warn('Update your start script to include the securenow preload:');
152
+ console.log('');
153
+ if (startScript.includes('node ')) {
154
+ const updated = startScript.replace('node ', 'node -r securenow/register ');
155
+ console.log(` "start": "${updated}"`);
156
+ } else {
157
+ console.log(` "start": "node -r securenow/register ${startScript.replace(/^node\s+/, '')}"`);
158
+ }
159
+ console.log('');
160
+ } else {
161
+ ui.warn('Add a start script with the securenow preload:');
162
+ console.log('');
163
+ console.log(' "start": "node -r securenow/register src/index.js"');
164
+ console.log('');
165
+ }
166
+ }
167
+
168
+ function initEnv(dir, flags) {
169
+ const envFiles = ['.env', '.env.local'];
170
+ let envPath = null;
171
+
172
+ for (const f of envFiles) {
173
+ const p = path.join(dir, f);
174
+ if (fs.existsSync(p)) {
175
+ const content = fs.readFileSync(p, 'utf8');
176
+ if (content.includes('SECURENOW_API_KEY')) {
177
+ ui.info(`SECURENOW_API_KEY already set in ${f}`);
178
+ return;
179
+ }
180
+ envPath = p;
181
+ break;
182
+ }
183
+ }
184
+
185
+ if (!envPath) envPath = path.join(dir, '.env.local');
186
+
187
+ const apiKey = flags.key || flags['api-key'] || '';
188
+ if (apiKey) {
189
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
190
+ const sep = existing && !existing.endsWith('\n') ? '\n' : '';
191
+ fs.appendFileSync(envPath, `${sep}SECURENOW_API_KEY=${apiKey}\n`, 'utf8');
192
+ ui.success(`Added SECURENOW_API_KEY to ${path.basename(envPath)}`);
193
+ } else {
194
+ ui.warn(`Add your API key to ${path.basename(envPath)}:`);
195
+ console.log('');
196
+ console.log(' SECURENOW_API_KEY=snk_live_...');
197
+ console.log('');
198
+ }
98
199
  }
99
200
 
100
201
  module.exports = { init };
package/cli/security.js CHANGED
@@ -197,6 +197,104 @@ async function blocklistStats(args, flags) {
197
197
  }
198
198
  }
199
199
 
200
+ // ── Allowlist ──
201
+
202
+ async function allowlistList(args, flags) {
203
+ requireAuth();
204
+ const s = ui.spinner('Fetching allowlist');
205
+ try {
206
+ const data = await api.get('/allowlist');
207
+ const items = data.allowedIps || [];
208
+ s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
209
+
210
+ if (flags.json) { ui.json(data); return; }
211
+
212
+ console.log('');
213
+ const rows = items.map(a => [
214
+ ui.c.dim(ui.truncate(a._id, 12)),
215
+ ui.c.green(a.ip || '—'),
216
+ a.label || ui.c.dim('—'),
217
+ ui.truncate(a.reason || '', 40),
218
+ ui.timeAgo(a.createdAt),
219
+ a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
220
+ ]);
221
+ ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'Added', 'Expires'], rows);
222
+ console.log('');
223
+ } catch (err) {
224
+ s.fail('Failed to fetch allowlist');
225
+ throw err;
226
+ }
227
+ }
228
+
229
+ async function allowlistAdd(args, flags) {
230
+ requireAuth();
231
+ let ip = args[0];
232
+ if (!ip) {
233
+ ip = await ui.prompt('IP address or CIDR to allow');
234
+ if (!ip) { ui.error('IP is required'); process.exit(1); }
235
+ }
236
+
237
+ const body = { ip };
238
+ if (flags.label) body.label = flags.label;
239
+ if (flags.reason) body.reason = flags.reason;
240
+
241
+ const s = ui.spinner(`Allowing ${ip}`);
242
+ try {
243
+ await api.post('/allowlist', body);
244
+ s.stop(`${ip} added to allowlist`);
245
+ } catch (err) {
246
+ s.fail('Failed to add to allowlist');
247
+ throw err;
248
+ }
249
+ }
250
+
251
+ async function allowlistRemove(args, flags) {
252
+ requireAuth();
253
+ const id = args[0];
254
+ if (!id) {
255
+ ui.error('Allowlist entry ID required. Usage: securenow allowlist remove <id>');
256
+ process.exit(1);
257
+ }
258
+
259
+ if (!flags.force && !flags.yes) {
260
+ const ok = await ui.confirm('Remove this IP from the allowlist?');
261
+ if (!ok) { ui.info('Cancelled'); return; }
262
+ }
263
+
264
+ const s = ui.spinner('Removing from allowlist');
265
+ try {
266
+ await api.delete(`/allowlist/${id}`);
267
+ s.stop('Removed from allowlist');
268
+ } catch (err) {
269
+ s.fail('Failed to remove from allowlist');
270
+ throw err;
271
+ }
272
+ }
273
+
274
+ async function allowlistStats(args, flags) {
275
+ requireAuth();
276
+ const s = ui.spinner('Fetching allowlist stats');
277
+ try {
278
+ const data = await api.get('/allowlist/stats');
279
+ const stats = data.stats || data;
280
+ s.stop('Stats loaded');
281
+
282
+ if (flags.json) { ui.json(stats); return; }
283
+
284
+ console.log('');
285
+ ui.heading('Allowlist Statistics');
286
+ console.log('');
287
+ ui.keyValue([
288
+ ['Total Active', String(stats.totalActive ?? '—')],
289
+ ['Total Removed', String(stats.totalRemoved ?? '—')],
290
+ ]);
291
+ console.log('');
292
+ } catch (err) {
293
+ s.fail('Failed to fetch stats');
294
+ throw err;
295
+ }
296
+ }
297
+
200
298
  // ── Trusted IPs ──
201
299
 
202
300
  async function trustedList(args, flags) {
@@ -810,6 +908,10 @@ module.exports = {
810
908
  blocklistAdd,
811
909
  blocklistRemove,
812
910
  blocklistStats,
911
+ allowlistList,
912
+ allowlistAdd,
913
+ allowlistRemove,
914
+ allowlistStats,
813
915
  trustedList,
814
916
  trustedAdd,
815
917
  trustedRemove,
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');
@@ -39,6 +39,12 @@ function parseArgs(argv) {
39
39
  // ── Command Registry ──
40
40
 
41
41
  const COMMANDS = {
42
+ init: {
43
+ desc: 'Set up SecureNow in the current project (instrumentation, config, env)',
44
+ usage: 'securenow init [--key <API_KEY>]',
45
+ flags: { key: 'API key to write to .env', 'api-key': 'Alias for --key' },
46
+ run: (a, f) => require('./cli/init').init(a, f),
47
+ },
42
48
  login: {
43
49
  desc: 'Authenticate with SecureNow',
44
50
  usage: 'securenow login [--token <TOKEN>]',
@@ -156,6 +162,17 @@ const COMMANDS = {
156
162
  },
157
163
  defaultSub: 'list',
158
164
  },
165
+ allowlist: {
166
+ desc: 'Manage IP allowlist (only allow listed IPs)',
167
+ usage: 'securenow allowlist <subcommand> [options]',
168
+ sub: {
169
+ list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
170
+ add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
171
+ remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
172
+ stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
173
+ },
174
+ defaultSub: 'list',
175
+ },
159
176
  trusted: {
160
177
  desc: 'Manage trusted IPs',
161
178
  usage: 'securenow trusted <subcommand> [options]',
@@ -334,7 +351,7 @@ function showHelp(commandName) {
334
351
  'Detect & Respond': ['issues', 'notifications', 'alerts', 'fp'],
335
352
  'Investigate': ['ip', 'forensics', 'api-map'],
336
353
  'Firewall': ['firewall'],
337
- 'Remediation': ['blocklist', 'trusted'],
354
+ 'Remediation': ['blocklist', 'allowlist', 'trusted'],
338
355
  'Settings': ['instances', 'config', 'version'],
339
356
  };
340
357
 
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Standalone firewall preload — no OpenTelemetry, no tracing.
5
+ *
6
+ * Usage:
7
+ * node -r securenow/firewall-only app.js
8
+ * NODE_OPTIONS='-r securenow/firewall-only' next start
9
+ *
10
+ * Reads .env via dotenv (if installed), then initialises the HTTP-level
11
+ * firewall when SECURENOW_API_KEY is present.
12
+ */
13
+
14
+ try { require('dotenv').config(); } catch (_) {}
15
+
16
+ const env = (k) =>
17
+ process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
18
+
19
+ const firewallApiKey = env('SECURENOW_API_KEY');
20
+
21
+ if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
22
+ require('./firewall').init({
23
+ apiKey: firewallApiKey,
24
+ apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
25
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
26
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
27
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
28
+ statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
29
+ log: env('SECURENOW_FIREWALL_LOG') !== '0',
30
+ tcp: env('SECURENOW_FIREWALL_TCP') === '1',
31
+ iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
32
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
33
+ });
34
+
35
+ const shutdown = () => { try { require('./firewall').shutdown(); } catch (_) {} };
36
+ process.on('SIGINT', shutdown);
37
+ process.on('SIGTERM', shutdown);
38
+ }
package/firewall-tcp.js CHANGED
@@ -13,20 +13,35 @@ const net = require('net');
13
13
  const { resolveSocketIp, isFromTrustedProxy } = require('./resolve-ip');
14
14
 
15
15
  let _getMatcher = null;
16
+ let _getAllowlistMatcher = null;
16
17
  let _options = null;
17
18
  let _patched = false;
18
19
  const _origListen = net.Server.prototype.listen;
19
20
 
20
21
  function onConnection(socket) {
21
- const matcher = _getMatcher();
22
- if (!matcher) return;
23
-
24
22
  const ip = resolveSocketIp(socket);
25
23
 
26
24
  // Skip if the connection is from a trusted proxy — Layer 1 will handle it
27
25
  // with proper X-Forwarded-For resolution
28
26
  if (isFromTrustedProxy(ip) || isFromTrustedProxy('::ffff:' + ip)) return;
29
27
 
28
+ // Allowlist check: if active, only listed IPs pass
29
+ const allowlistMatcher = _getAllowlistMatcher ? _getAllowlistMatcher() : null;
30
+ if (allowlistMatcher && allowlistMatcher.stats().total > 0) {
31
+ if (!allowlistMatcher.isBlocked(ip)) {
32
+ if (_options && _options.log) {
33
+ console.log('[securenow] Firewall: blocked %s via TCP (not in allowlist)', ip);
34
+ }
35
+ socket.destroy();
36
+ return;
37
+ }
38
+ return; // on allowlist — allow
39
+ }
40
+
41
+ // Blocklist check
42
+ const matcher = _getMatcher();
43
+ if (!matcher) return;
44
+
30
45
  if (matcher.isBlocked(ip)) {
31
46
  if (_options && _options.log) {
32
47
  console.log('[securenow] Firewall: blocked %s via TCP (socket destroyed)', ip);
@@ -35,8 +50,9 @@ function onConnection(socket) {
35
50
  }
36
51
  }
37
52
 
38
- function init(getMatcher, options) {
53
+ function init(getMatcher, options, getAllowlistMatcher) {
39
54
  _getMatcher = getMatcher;
55
+ _getAllowlistMatcher = getAllowlistMatcher || null;
40
56
  _options = options;
41
57
 
42
58
  if (_patched) return;
package/firewall.js CHANGED
@@ -17,6 +17,15 @@ let _consecutiveErrors = 0;
17
17
  let _layers = [];
18
18
  let _rawIps = [];
19
19
  let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
20
+ let _localhostFallbackTried = false;
21
+
22
+ // Allowlist state
23
+ let _allowlistMatcher = null;
24
+ let _allowlistRawIps = [];
25
+ let _lastAllowlistModified = null;
26
+ let _lastAllowlistVersion = null;
27
+ let _lastAllowlistSyncEtag = null;
28
+ let _allowlistVersionTimer = null;
20
29
 
21
30
  // ────── Blocklist Sync ──────
22
31
 
@@ -87,6 +96,136 @@ function notifyLayers(ips) {
87
96
  }
88
97
  }
89
98
 
99
+ // ────── Allowlist Sync ──────
100
+
101
+ function syncAllowlist(callback) {
102
+ const url = buildUrl(_options.apiUrl, '/firewall/allowlist');
103
+ const mod = url.startsWith('https') ? https : http;
104
+ const parsed = new URL(url);
105
+
106
+ const reqOptions = {
107
+ hostname: parsed.hostname,
108
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
109
+ path: parsed.pathname + parsed.search,
110
+ method: 'GET',
111
+ headers: {
112
+ 'Authorization': `Bearer ${_options.apiKey}`,
113
+ 'User-Agent': 'securenow-firewall-sdk',
114
+ },
115
+ timeout: 10000,
116
+ };
117
+
118
+ if (_lastAllowlistSyncEtag) {
119
+ reqOptions.headers['If-None-Match'] = _lastAllowlistSyncEtag;
120
+ } else if (_lastAllowlistModified) {
121
+ reqOptions.headers['If-Modified-Since'] = _lastAllowlistModified;
122
+ }
123
+
124
+ const req = mod.request(reqOptions, (res) => {
125
+ if (res.statusCode === 304) {
126
+ callback(null, false);
127
+ return;
128
+ }
129
+
130
+ let data = '';
131
+ res.on('data', (chunk) => { data += chunk; });
132
+ res.on('end', () => {
133
+ if (res.statusCode !== 200) {
134
+ callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
135
+ return;
136
+ }
137
+ try {
138
+ const body = JSON.parse(data);
139
+ const ips = body.ips || [];
140
+ _allowlistRawIps = ips;
141
+ _allowlistMatcher = createMatcher(ips);
142
+ _lastAllowlistModified = res.headers['last-modified'] || null;
143
+ if (res.headers['etag']) _lastAllowlistSyncEtag = res.headers['etag'];
144
+ callback(null, true, _allowlistMatcher.stats());
145
+ } catch (e) {
146
+ callback(new Error(`Failed to parse allowlist response: ${e.message}`));
147
+ }
148
+ });
149
+ });
150
+
151
+ req.on('error', (err) => callback(err));
152
+ req.on('timeout', () => { req.destroy(); callback(new Error('Allowlist sync request timed out')); });
153
+ req.end();
154
+ }
155
+
156
+ function checkAllowlistVersion(callback) {
157
+ const url = buildUrl(_options.apiUrl, '/firewall/allowlist/version');
158
+ const mod = url.startsWith('https') ? https : http;
159
+ const parsed = new URL(url);
160
+
161
+ const headers = {
162
+ 'Authorization': `Bearer ${_options.apiKey}`,
163
+ 'User-Agent': 'securenow-firewall-sdk',
164
+ };
165
+ if (_lastAllowlistVersion) headers['If-None-Match'] = _lastAllowlistVersion;
166
+
167
+ const req = mod.request({
168
+ hostname: parsed.hostname,
169
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
170
+ path: parsed.pathname + parsed.search,
171
+ method: 'GET',
172
+ headers,
173
+ timeout: 5000,
174
+ }, (res) => {
175
+ if (res.statusCode === 304) {
176
+ res.resume();
177
+ callback(null, false);
178
+ return;
179
+ }
180
+
181
+ let data = '';
182
+ res.on('data', (chunk) => { data += chunk; });
183
+ res.on('end', () => {
184
+ if (res.statusCode !== 200) {
185
+ callback(null, false);
186
+ return;
187
+ }
188
+ try {
189
+ const body = JSON.parse(data);
190
+ const version = body.version || null;
191
+ const changed = version !== _lastAllowlistVersion;
192
+ if (changed) _lastAllowlistVersion = version;
193
+ callback(null, changed);
194
+ } catch (_e) { callback(null, false); }
195
+ });
196
+ });
197
+
198
+ req.on('error', () => { callback(null, false); });
199
+ req.on('timeout', () => { req.destroy(); callback(null, false); });
200
+ req.end();
201
+ }
202
+
203
+ function doFullAllowlistSync() {
204
+ syncAllowlist((err, changed, stats) => {
205
+ if (err) {
206
+ if (_options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err.message);
207
+ } else if (changed && stats && _options.log) {
208
+ console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
209
+ }
210
+ });
211
+ }
212
+
213
+ function scheduleNextAllowlistVersionCheck() {
214
+ const baseMs = (_options.versionCheckInterval || 10) * 1000;
215
+ const delayMs = jitter(baseMs);
216
+
217
+ _allowlistVersionTimer = setTimeout(() => {
218
+ checkAllowlistVersion((_err, changed) => {
219
+ if (changed) {
220
+ if (_options.log) console.log('[securenow] Firewall: allowlist version changed, syncing…');
221
+ doFullAllowlistSync();
222
+ }
223
+ scheduleNextAllowlistVersionCheck();
224
+ });
225
+ }, delayMs);
226
+ if (_allowlistVersionTimer.unref) _allowlistVersionTimer.unref();
227
+ }
228
+
90
229
  function checkVersion(callback) {
91
230
  const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
92
231
  const mod = url.startsWith('https') ? https : http;
@@ -178,6 +317,16 @@ function startSyncLoop() {
178
317
  function initialSync() {
179
318
  syncBlocklist((err, changed, stats) => {
180
319
  if (err) {
320
+ const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
321
+ if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
322
+ _localhostFallbackTried = true;
323
+ const origUrl = _options.apiUrl;
324
+ _options.apiUrl = 'http://localhost:4000';
325
+ if (_options.log) console.log('[securenow] Firewall: %s unreachable, trying http://localhost:4000', origUrl);
326
+ const retryTimer = setTimeout(initialSync, 1000);
327
+ if (retryTimer.unref) retryTimer.unref();
328
+ return;
329
+ }
181
330
  if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
182
331
  if (_options.failMode === 'closed') {
183
332
  _matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
@@ -193,11 +342,27 @@ function startSyncLoop() {
193
342
  });
194
343
  }
195
344
 
345
+ function initialAllowlistSync() {
346
+ syncAllowlist((err, changed, stats) => {
347
+ if (err) {
348
+ if (_options.log) console.warn('[securenow] Firewall: initial allowlist sync failed:', err.message);
349
+ const retryTimer = setTimeout(initialAllowlistSync, RETRY_DELAY);
350
+ if (retryTimer.unref) retryTimer.unref();
351
+ return;
352
+ }
353
+ if (changed && stats && stats.total > 0) {
354
+ if (_options.log) console.log('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
355
+ }
356
+ });
357
+ }
358
+
196
359
  initialSync();
360
+ initialAllowlistSync();
197
361
 
198
362
  scheduleNextVersionCheck();
363
+ scheduleNextAllowlistVersionCheck();
199
364
 
200
- _syncTimer = setInterval(() => { doFullSync(); }, fullSyncIntervalMs);
365
+ _syncTimer = setInterval(() => { doFullSync(); doFullAllowlistSync(); }, fullSyncIntervalMs);
201
366
  if (_syncTimer.unref) _syncTimer.unref();
202
367
  }
203
368
 
@@ -255,24 +420,41 @@ function wrapListener(originalListener) {
255
420
  };
256
421
  }
257
422
 
423
+ function sendBlockResponse(req, res, ip) {
424
+ const code = (_options && _options.statusCode) || 403;
425
+ const accept = req.headers['accept'] || '';
426
+ if (accept.includes('text/html')) {
427
+ res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
428
+ res.end(blockedHtml(ip));
429
+ } else {
430
+ res.writeHead(code, { 'Content-Type': 'application/json' });
431
+ res.end(JSON.stringify({ error: 'Forbidden', ip }));
432
+ }
433
+ }
434
+
258
435
  function firewallRequestHandler(req, res) {
259
- if (_matcher) {
260
- const ip = resolveClientIp(req);
261
- if (_matcher.isBlocked(ip)) {
436
+ const ip = resolveClientIp(req);
437
+
438
+ // Allowlist check: if active, only listed IPs are allowed through
439
+ if (_allowlistMatcher && _allowlistMatcher.stats().total > 0) {
440
+ if (!_allowlistMatcher.isBlocked(ip)) {
262
441
  _stats.blocked++;
263
- if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
264
- const code = (_options && _options.statusCode) || 403;
265
- const accept = req.headers['accept'] || '';
266
- if (accept.includes('text/html')) {
267
- res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
268
- res.end(blockedHtml(ip));
269
- } else {
270
- res.writeHead(code, { 'Content-Type': 'application/json' });
271
- res.end(JSON.stringify({ error: 'Forbidden', ip }));
272
- }
442
+ if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP (not in allowlist)', ip);
443
+ sendBlockResponse(req, res, ip);
273
444
  return true;
274
445
  }
446
+ // IP is on the allowlist — skip blocklist check, allow through
447
+ return false;
275
448
  }
449
+
450
+ // Blocklist check
451
+ if (_matcher && _matcher.isBlocked(ip)) {
452
+ _stats.blocked++;
453
+ if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
454
+ sendBlockResponse(req, res, ip);
455
+ return true;
456
+ }
457
+
276
458
  return false;
277
459
  }
278
460
 
@@ -330,7 +512,7 @@ function init(options) {
330
512
  if (_options.tcp) {
331
513
  try {
332
514
  const tcpLayer = require('./firewall-tcp');
333
- tcpLayer.init(() => _matcher, _options);
515
+ tcpLayer.init(() => _matcher, _options, () => _allowlistMatcher);
334
516
  _layers.push(tcpLayer);
335
517
  if (_options.log) console.log('[securenow] Firewall: Layer 2 (TCP drop) active');
336
518
  } catch (e) {
@@ -373,6 +555,7 @@ function init(options) {
373
555
 
374
556
  function shutdown() {
375
557
  if (_versionTimer) { clearTimeout(_versionTimer); _versionTimer = null; }
558
+ if (_allowlistVersionTimer) { clearTimeout(_allowlistVersionTimer); _allowlistVersionTimer = null; }
376
559
  if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
377
560
 
378
561
  for (const layer of _layers) {
@@ -392,9 +575,15 @@ function shutdown() {
392
575
  }
393
576
 
394
577
  function getStats() {
395
- return { ..._stats, matcher: _matcher ? _matcher.stats() : null, initialized: _initialized };
578
+ return {
579
+ ..._stats,
580
+ matcher: _matcher ? _matcher.stats() : null,
581
+ allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
582
+ initialized: _initialized,
583
+ };
396
584
  }
397
585
 
398
586
  function getMatcher() { return _matcher; }
587
+ function getAllowlistMatcher() { return _allowlistMatcher; }
399
588
 
400
- module.exports = { init, shutdown, getStats, getMatcher };
589
+ module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher };
@@ -1,33 +1,100 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Next.js configuration helpers for SecureNow
5
+ *
6
+ * Usage (recommended — zero-list approach):
7
+ *
8
+ * const { withSecureNow } = require('securenow/nextjs-webpack-config');
9
+ * module.exports = withSecureNow({
10
+ * // your existing next.config options
11
+ * });
12
+ *
13
+ * Legacy webpack-only helper (still exported for backwards compat):
14
+ *
15
+ * const { getSecureNowWebpackConfig } = require('securenow/nextjs-webpack-config');
16
+ * module.exports = { webpack: (config, opts) => getSecureNowWebpackConfig(config, opts) };
17
+ */
18
+
19
+ const EXTERNAL_PACKAGES = [
20
+ 'securenow',
21
+ '@opentelemetry/sdk-node',
22
+ '@opentelemetry/auto-instrumentations-node',
23
+ '@opentelemetry/instrumentation-http',
24
+ '@opentelemetry/exporter-trace-otlp-http',
25
+ '@opentelemetry/exporter-logs-otlp-http',
26
+ '@opentelemetry/sdk-logs',
27
+ '@opentelemetry/instrumentation',
28
+ '@opentelemetry/resources',
29
+ '@opentelemetry/semantic-conventions',
30
+ '@opentelemetry/api',
31
+ '@opentelemetry/api-logs',
32
+ '@vercel/otel',
33
+ ];
34
+
35
+ function detectNextMajor() {
36
+ try {
37
+ const pkg = require('next/package.json');
38
+ return parseInt(pkg.version, 10) || 14;
39
+ } catch {
40
+ return 14;
41
+ }
42
+ }
43
+
1
44
  /**
2
- * Next.js webpack configuration for SecureNow
3
- *
4
- * Add this to your next.config.js to suppress OpenTelemetry instrumentation warnings
5
- *
6
- * Usage:
7
- * const { getSecureNowWebpackConfig } = require('securenow/nextjs-webpack-config');
8
- *
9
- * module.exports = {
10
- * webpack: (config, options) => {
11
- * return getSecureNowWebpackConfig(config, options);
12
- * }
13
- * };
45
+ * Wrap a Next.js config object to auto-externalize SecureNow + OTel
46
+ * packages and enable the instrumentation hook.
47
+ *
48
+ * module.exports = withSecureNow({ reactStrictMode: true });
14
49
  */
50
+ function withSecureNow(userConfig) {
51
+ if (typeof userConfig === 'function') {
52
+ return (...args) => withSecureNow(userConfig(...args));
53
+ }
54
+
55
+ const cfg = { ...userConfig };
56
+ const major = detectNextMajor();
57
+
58
+ if (major >= 15) {
59
+ cfg.serverExternalPackages = dedup([
60
+ ...(cfg.serverExternalPackages || []),
61
+ ...EXTERNAL_PACKAGES,
62
+ ]);
63
+ } else {
64
+ cfg.experimental = { ...(cfg.experimental || {}) };
65
+ cfg.experimental.instrumentationHook = true;
66
+ cfg.experimental.serverComponentsExternalPackages = dedup([
67
+ ...(cfg.experimental.serverComponentsExternalPackages || []),
68
+ ...EXTERNAL_PACKAGES,
69
+ ]);
70
+ }
71
+
72
+ const origWebpack = cfg.webpack;
73
+ cfg.webpack = (config, options) => {
74
+ const c = origWebpack ? origWebpack(config, options) : config;
75
+ return getSecureNowWebpackConfig(c, options);
76
+ };
77
+
78
+ return cfg;
79
+ }
15
80
 
81
+ function dedup(arr) {
82
+ return [...new Set(arr)];
83
+ }
84
+
85
+ /**
86
+ * Legacy: suppress OTel webpack warnings and add externals.
87
+ */
16
88
  function getSecureNowWebpackConfig(config, options) {
17
89
  const { isServer } = options;
18
-
19
- // Only apply to server-side builds
90
+
20
91
  if (isServer) {
21
- // Suppress warnings for OpenTelemetry instrumentations
22
92
  config.ignoreWarnings = config.ignoreWarnings || [];
23
-
24
93
  config.ignoreWarnings.push(
25
- // Ignore "Critical dependency" warnings from instrumentations
26
94
  {
27
95
  module: /@opentelemetry\/instrumentation/,
28
96
  message: /Critical dependency: the request of a dependency is an expression/,
29
97
  },
30
- // Ignore missing optional peer dependencies
31
98
  {
32
99
  module: /@opentelemetry/,
33
100
  message: /Module not found.*@opentelemetry\/winston-transport/,
@@ -35,43 +102,11 @@ function getSecureNowWebpackConfig(config, options) {
35
102
  {
36
103
  module: /@opentelemetry/,
37
104
  message: /Module not found.*@opentelemetry\/exporter-jaeger/,
38
- }
105
+ },
39
106
  );
40
-
41
- // Externalize problematic packages (don't bundle them)
42
- config.externals = config.externals || [];
43
-
44
- // Add OpenTelemetry packages as externals
45
- if (typeof config.externals === 'function') {
46
- const originalExternals = config.externals;
47
- config.externals = async (...args) => {
48
- const result = await originalExternals(...args);
49
- if (result) return result;
50
-
51
- const [context, request] = args;
52
-
53
- // Externalize OpenTelemetry instrumentation packages
54
- if (request.startsWith('@opentelemetry/')) {
55
- return `commonjs ${request}`;
56
- }
57
-
58
- return undefined;
59
- };
60
- } else if (Array.isArray(config.externals)) {
61
- config.externals.push(/@opentelemetry\//);
62
- } else {
63
- config.externals = [/@opentelemetry\//];
64
- }
65
107
  }
66
-
108
+
67
109
  return config;
68
110
  }
69
111
 
70
- module.exports = { getSecureNowWebpackConfig };
71
-
72
-
73
-
74
-
75
-
76
-
77
-
112
+ module.exports = { withSecureNow, getSecureNowWebpackConfig, EXTERNAL_PACKAGES };
package/nextjs.js CHANGED
@@ -541,28 +541,6 @@ function registerSecureNow(options = {}) {
541
541
  }
542
542
  } catch (_) {}
543
543
 
544
- // Firewall — auto-activates when SECURENOW_API_KEY is set
545
- const firewallApiKey = env('SECURENOW_API_KEY');
546
- if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
547
- try {
548
- require('./firewall').init({
549
- apiKey: firewallApiKey,
550
- apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
551
- versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
552
- syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
553
- failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
554
- statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
555
- log: env('SECURENOW_FIREWALL_LOG') !== '0',
556
- tcp: env('SECURENOW_FIREWALL_TCP') === '1',
557
- iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
558
- cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
559
- });
560
- } catch (e) {
561
- console.warn('[securenow] Firewall init failed:', e.message);
562
- }
563
- }
564
-
565
-
566
544
  console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
567
545
  console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
568
546
  console.log('[securenow] • IP addresses (x-forwarded-for, x-real-ip, socket)');
@@ -594,6 +572,27 @@ function registerSecureNow(options = {}) {
594
572
  console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
595
573
  }
596
574
  }
575
+
576
+ // Firewall — runs independently from OTel so it works even if tracing fails
577
+ const firewallApiKey = env('SECURENOW_API_KEY');
578
+ if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
579
+ try {
580
+ require('./firewall').init({
581
+ apiKey: firewallApiKey,
582
+ apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
583
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
584
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
585
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
586
+ statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
587
+ log: env('SECURENOW_FIREWALL_LOG') !== '0',
588
+ tcp: env('SECURENOW_FIREWALL_TCP') === '1',
589
+ iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
590
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
591
+ });
592
+ } catch (e) {
593
+ console.warn('[securenow] Firewall init failed:', e.message);
594
+ }
595
+ }
597
596
  }
598
597
 
599
598
  module.exports = {
@@ -324,11 +324,34 @@ export default defineNitroPlugin((nitroApp) => {
324
324
  // not critical
325
325
  }
326
326
 
327
+ // ── Firewall — runs independently from OTel ──
328
+ const firewallApiKey = env('SECURENOW_API_KEY');
329
+ if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
330
+ try {
331
+ const { init: fwInit } = await import('./firewall.js');
332
+ fwInit({
333
+ apiKey: firewallApiKey,
334
+ apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
335
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
336
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
337
+ failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
338
+ statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
339
+ log: env('SECURENOW_FIREWALL_LOG') !== '0',
340
+ tcp: env('SECURENOW_FIREWALL_TCP') === '1',
341
+ iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
342
+ cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
343
+ });
344
+ } catch (e) {
345
+ console.warn('[securenow] Firewall init failed:', e.message);
346
+ }
347
+ }
348
+
327
349
  // ── Graceful shutdown ──
328
350
  const shutdown = async (sig) => {
329
351
  try {
330
352
  await sdk.shutdown?.();
331
353
  if (loggerProvider) await loggerProvider.shutdown?.();
354
+ try { const fw = await import('./firewall.js'); fw.shutdown?.(); } catch {}
332
355
  console.log(`[securenow] Shut down on ${sig}`);
333
356
  } catch {
334
357
  // swallow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.12.1",
3
+ "version": "5.14.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",
@@ -81,6 +81,9 @@
81
81
  "./firewall": {
82
82
  "default": "./firewall.js"
83
83
  },
84
+ "./firewall-only": {
85
+ "default": "./firewall-only.js"
86
+ },
84
87
  "./cidr": {
85
88
  "default": "./cidr.js"
86
89
  },
@@ -117,6 +120,7 @@
117
120
  "resolve-ip.js",
118
121
  "cidr.js",
119
122
  "firewall.js",
123
+ "firewall-only.js",
120
124
  "firewall-tcp.js",
121
125
  "firewall-iptables.js",
122
126
  "firewall-cloud.js",