securenow 5.11.1 → 5.12.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
@@ -8,11 +8,14 @@ const { resolveClientIp } = require('./resolve-ip');
8
8
  let _options = null;
9
9
  let _matcher = null;
10
10
  let _syncTimer = null;
11
+ let _versionTimer = null;
11
12
  let _lastModified = null;
13
+ let _lastVersion = null;
12
14
  let _initialized = false;
15
+ let _consecutiveErrors = 0;
13
16
  let _layers = [];
14
17
  let _rawIps = [];
15
- let _stats = { syncs: 0, blocked: 0, allowed: 0 };
18
+ let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
16
19
 
17
20
  // ────── Blocklist Sync ──────
18
21
 
@@ -37,7 +40,9 @@ function syncBlocklist(callback) {
37
40
  timeout: 10000,
38
41
  };
39
42
 
40
- if (_lastModified) {
43
+ if (_lastVersion) {
44
+ reqOptions.headers['If-None-Match'] = _lastVersion;
45
+ } else if (_lastModified) {
41
46
  reqOptions.headers['If-Modified-Since'] = _lastModified;
42
47
  }
43
48
 
@@ -60,6 +65,7 @@ function syncBlocklist(callback) {
60
65
  _rawIps = ips;
61
66
  _matcher = createMatcher(ips);
62
67
  _lastModified = res.headers['last-modified'] || null;
68
+ if (res.headers['etag']) _lastVersion = res.headers['etag'];
63
69
  _stats.syncs++;
64
70
  notifyLayers(ips);
65
71
  callback(null, true, _matcher.stats());
@@ -80,32 +86,117 @@ function notifyLayers(ips) {
80
86
  }
81
87
  }
82
88
 
83
- function startSyncLoop() {
84
- const intervalMs = (_options.syncInterval || 60) * 1000;
89
+ function checkVersion(callback) {
90
+ const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
91
+ const mod = url.startsWith('https') ? https : http;
92
+ const parsed = new URL(url);
93
+
94
+ const headers = {
95
+ 'Authorization': `Bearer ${_options.apiKey}`,
96
+ 'User-Agent': 'securenow-firewall-sdk',
97
+ };
98
+ if (_lastVersion) headers['If-None-Match'] = _lastVersion;
99
+
100
+ const req = mod.request({
101
+ hostname: parsed.hostname,
102
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
103
+ path: parsed.pathname + parsed.search,
104
+ method: 'GET',
105
+ headers,
106
+ timeout: 5000,
107
+ }, (res) => {
108
+ _stats.versionChecks++;
109
+
110
+ if (res.statusCode === 304) {
111
+ _consecutiveErrors = 0;
112
+ res.resume();
113
+ callback(null, false);
114
+ return;
115
+ }
116
+
117
+ let data = '';
118
+ res.on('data', (chunk) => { data += chunk; });
119
+ res.on('end', () => {
120
+ if (res.statusCode !== 200) {
121
+ _consecutiveErrors++;
122
+ _stats.errors++;
123
+ callback(null, false);
124
+ return;
125
+ }
126
+ _consecutiveErrors = 0;
127
+ try {
128
+ const body = JSON.parse(data);
129
+ const version = body.version || null;
130
+ const changed = version !== _lastVersion;
131
+ if (changed) _lastVersion = version;
132
+ callback(null, changed);
133
+ } catch (_e) { callback(null, false); }
134
+ });
135
+ });
136
+
137
+ req.on('error', () => { _consecutiveErrors++; _stats.errors++; callback(null, false); });
138
+ req.on('timeout', () => { req.destroy(); _consecutiveErrors++; _stats.errors++; callback(null, false); });
139
+ req.end();
140
+ }
85
141
 
142
+ function doFullSync() {
86
143
  syncBlocklist((err, changed, stats) => {
87
144
  if (err) {
88
- if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
89
- if (_options.failMode === 'closed') {
90
- _matcher = createMatcher([]); // block everything by default via empty matcher
91
- _matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
92
- }
93
- } else if (changed && stats) {
94
- if (_options.log) console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
145
+ if (_options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
146
+ } else if (changed && stats && _options.log) {
147
+ console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
95
148
  }
96
- _initialized = true;
97
149
  });
150
+ }
98
151
 
99
- _syncTimer = setInterval(() => {
152
+ function jitter(baseMs) {
153
+ return baseMs * (0.8 + Math.random() * 0.4);
154
+ }
155
+
156
+ function scheduleNextVersionCheck() {
157
+ const baseMs = (_options.versionCheckInterval || 10) * 1000;
158
+ const backoffMs = Math.min(baseMs * Math.pow(2, _consecutiveErrors), 120_000);
159
+ const delayMs = jitter(backoffMs);
160
+
161
+ _versionTimer = setTimeout(() => {
162
+ checkVersion((_err, changed) => {
163
+ if (changed) {
164
+ if (_options.log) console.log('[securenow] Firewall: blocklist version changed, syncing…');
165
+ doFullSync();
166
+ }
167
+ scheduleNextVersionCheck();
168
+ });
169
+ }, delayMs);
170
+ if (_versionTimer.unref) _versionTimer.unref();
171
+ }
172
+
173
+ function startSyncLoop() {
174
+ const fullSyncIntervalMs = (_options.syncInterval || 300) * 1000;
175
+ const RETRY_DELAY = 5000;
176
+
177
+ function initialSync() {
100
178
  syncBlocklist((err, changed, stats) => {
101
179
  if (err) {
102
- if (_options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
103
- } else if (changed && stats && _options.log) {
104
- console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
180
+ if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
181
+ if (_options.failMode === 'closed') {
182
+ _matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
183
+ }
184
+ const retryTimer = setTimeout(initialSync, RETRY_DELAY);
185
+ if (retryTimer.unref) retryTimer.unref();
186
+ return;
187
+ }
188
+ if (changed && stats) {
189
+ if (_options.log) console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
105
190
  }
191
+ _initialized = true;
106
192
  });
107
- }, intervalMs);
193
+ }
194
+
195
+ initialSync();
108
196
 
197
+ scheduleNextVersionCheck();
198
+
199
+ _syncTimer = setInterval(() => { doFullSync(); }, fullSyncIntervalMs);
109
200
  if (_syncTimer.unref) _syncTimer.unref();
110
201
  }
111
202
 
@@ -115,21 +206,87 @@ const _origHttpCreate = http.createServer;
115
206
  const _origHttpsCreate = https.createServer;
116
207
  let _httpPatched = false;
117
208
 
209
+ function blockedHtml(ip) {
210
+ const maskedIp = ip || 'unknown';
211
+ return `<!DOCTYPE html>
212
+ <html lang="en">
213
+ <head>
214
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
215
+ <title>Access Blocked — Security Alert</title>
216
+ <style>
217
+ *{margin:0;padding:0;box-sizing:border-box}
218
+ body{min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0a0a0a;color:#e5e5e5}
219
+ .wrap{text-align:center;max-width:540px;padding:2.5rem 2rem}
220
+ .icon{width:64px;height:64px;margin:0 auto 1.5rem;border-radius:50%;background:rgba(220,38,38,.12);display:flex;align-items:center;justify-content:center}
221
+ .icon svg{width:32px;height:32px;fill:none;stroke:#dc2626;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
222
+ .badge{display:inline-block;padding:.25rem .75rem;border-radius:999px;background:rgba(220,38,38,.15);color:#f87171;font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;margin-bottom:1.25rem}
223
+ h1{font-size:1.6rem;font-weight:700;margin-bottom:.6rem;color:#fff}
224
+ .sub{font-size:1rem;color:#f87171;font-weight:600;margin-bottom:1.25rem}
225
+ p{font-size:.9rem;line-height:1.7;color:#a1a1aa;margin-bottom:.5rem}
226
+ .ip-box{display:inline-block;margin:1rem 0;padding:.6rem 1.5rem;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(220,38,38,.25);font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;font-size:.95rem;color:#fca5a5;letter-spacing:.03em}
227
+ .divider{width:48px;height:2px;background:rgba(220,38,38,.3);margin:1.5rem auto}
228
+ .contact{font-size:.85rem;color:#71717a;line-height:1.7}
229
+ .contact a{color:#f87171;text-decoration:none;font-weight:500}
230
+ .contact a:hover{text-decoration:underline}
231
+ .footer{margin-top:2rem;font-size:.7rem;color:#3f3f46}
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <div class="wrap">
236
+ <div class="icon"><svg viewBox="0 0 24 24"><path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg></div>
237
+ <span class="badge">Security Alert</span>
238
+ <h1>Access Blocked</h1>
239
+ <p class="sub">Malicious activity detected from your IP address</p>
240
+ <p>Your IP has been flagged and blocked due to suspicious or malicious behavior originating from your network. Our security team has been notified and is actively investigating.</p>
241
+ <div class="ip-box">${maskedIp}</div>
242
+ <div class="divider"></div>
243
+ <p class="contact">If you believe this is a mistake, please contact us at<br><a href="mailto:contact@securenow.ai?subject=Blocked%20IP%20Appeal%20-%20${encodeURIComponent(maskedIp)}&body=IP:%20${encodeURIComponent(maskedIp)}%0ATimestamp:%20${encodeURIComponent(new Date().toISOString())}%0A%0APlease%20describe%20why%20you%20believe%20this%20block%20is%20incorrect:">contact@securenow.ai</a><br>Include your IP address and the time of this incident.</p>
244
+ <p class="footer">Ref: ${maskedIp} &mdash; ${new Date().toISOString()} &mdash; HTTP 403</p>
245
+ </div>
246
+ </body>
247
+ </html>`;
248
+ }
249
+
118
250
  function wrapListener(originalListener) {
119
251
  return function firewallGuard(req, res) {
120
- if (_matcher) {
121
- const ip = resolveClientIp(req);
122
- if (_matcher.isBlocked(ip)) {
123
- _stats.blocked++;
124
- if (_options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
125
- const code = _options.statusCode || 403;
252
+ _stats.allowed++;
253
+ return originalListener.call(this, req, res);
254
+ };
255
+ }
256
+
257
+ function firewallRequestHandler(req, res) {
258
+ if (_matcher) {
259
+ const ip = resolveClientIp(req);
260
+ if (_matcher.isBlocked(ip)) {
261
+ _stats.blocked++;
262
+ if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
263
+ const code = (_options && _options.statusCode) || 403;
264
+ const accept = req.headers['accept'] || '';
265
+ if (accept.includes('text/html')) {
266
+ res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
267
+ res.end(blockedHtml(ip));
268
+ } else {
126
269
  res.writeHead(code, { 'Content-Type': 'application/json' });
127
- res.end(JSON.stringify({ error: 'Forbidden' }));
128
- return;
270
+ res.end(JSON.stringify({ error: 'Forbidden', ip }));
129
271
  }
272
+ return true;
130
273
  }
131
- _stats.allowed++;
132
- return originalListener.call(this, req, res);
274
+ }
275
+ return false;
276
+ }
277
+
278
+ const _origEmit = http.Server.prototype.emit;
279
+ let _emitPatched = false;
280
+
281
+ function patchEmitLayer() {
282
+ if (_emitPatched) return;
283
+ _emitPatched = true;
284
+
285
+ http.Server.prototype.emit = function(event, req, res, ...rest) {
286
+ if (event === 'request' && req && res && !res.headersSent) {
287
+ if (firewallRequestHandler(req, res)) return true;
288
+ }
289
+ return _origEmit.call(this, event, req, res, ...rest);
133
290
  };
134
291
  }
135
292
 
@@ -137,6 +294,9 @@ function patchHttpLayer() {
137
294
  if (_httpPatched) return;
138
295
  _httpPatched = true;
139
296
 
297
+ // Patch Server.prototype.emit to intercept requests on already-created servers
298
+ patchEmitLayer();
299
+
140
300
  http.createServer = function(...args) {
141
301
  if (typeof args[args.length - 1] === 'function') {
142
302
  args[args.length - 1] = wrapListener(args[args.length - 1]);
@@ -211,6 +371,7 @@ function init(options) {
211
371
  }
212
372
 
213
373
  function shutdown() {
374
+ if (_versionTimer) { clearTimeout(_versionTimer); _versionTimer = null; }
214
375
  if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
215
376
 
216
377
  for (const layer of _layers) {
@@ -218,7 +379,10 @@ function shutdown() {
218
379
  }
219
380
  _layers = [];
220
381
 
221
- // Restore original createServer
382
+ if (_emitPatched) {
383
+ http.Server.prototype.emit = _origEmit;
384
+ _emitPatched = false;
385
+ }
222
386
  if (_httpPatched) {
223
387
  http.createServer = _origHttpCreate;
224
388
  https.createServer = _origHttpsCreate;
package/nextjs.js CHANGED
@@ -37,7 +37,7 @@
37
37
  * SECURENOW_INSTANCE=http://your-otlp-backend:4318
38
38
  */
39
39
 
40
- const { v4: uuidv4 } = require('uuid');
40
+ const { randomUUID } = require('crypto');
41
41
 
42
42
  const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
43
43
 
@@ -157,9 +157,9 @@ function registerSecureNow(options = {}) {
157
157
  // service.name
158
158
  let serviceName;
159
159
  if (baseName) {
160
- serviceName = noUuid ? baseName : `${baseName}-${uuidv4()}`;
160
+ serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
161
161
  } else {
162
- serviceName = `nextjs-app-${uuidv4()}`;
162
+ serviceName = `nextjs-app-${randomUUID()}`;
163
163
  console.warn('[securenow] ⚠️ No SECURENOW_APPID or OTEL_SERVICE_NAME provided. Using fallback: %s', serviceName);
164
164
  console.warn('[securenow] 💡 Set SECURENOW_APPID=your-app-name in .env.local for better tracking');
165
165
  }
@@ -548,7 +548,8 @@ function registerSecureNow(options = {}) {
548
548
  require('./firewall').init({
549
549
  apiKey: firewallApiKey,
550
550
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
551
- syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 60,
551
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
552
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
552
553
  failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
553
554
  statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
554
555
  log: env('SECURENOW_FIREWALL_LOG') !== '0',
@@ -561,6 +562,7 @@ function registerSecureNow(options = {}) {
561
562
  }
562
563
  }
563
564
 
565
+
564
566
  console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
565
567
  console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
566
568
  console.log('[securenow] • IP addresses (x-forwarded-for, x-real-ip, socket)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.11.1",
3
+ "version": "5.12.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
@@ -556,7 +556,8 @@ const sdk = new NodeSDK({
556
556
  require('./firewall').init({
557
557
  apiKey: firewallApiKey,
558
558
  apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
559
- syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 60,
559
+ versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
560
+ syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
560
561
  failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
561
562
  statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
562
563
  log: env('SECURENOW_FIREWALL_LOG') !== '0',