securenow 5.12.2 → 5.15.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/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 };
@@ -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.2",
3
+ "version": "5.15.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",
@@ -127,7 +131,9 @@
127
131
  "docs/",
128
132
  "README.md",
129
133
  "NPM_README.md",
130
- "CONSUMING-APPS-GUIDE.md"
134
+ "CONSUMING-APPS-GUIDE.md",
135
+ "SKILL-CLI.md",
136
+ "SKILL-API.md"
131
137
  ],
132
138
  "dependencies": {
133
139
  "@opentelemetry/api": "1.7.0",