securenow 5.16.3 → 5.17.1

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/SKILL-API.md CHANGED
@@ -460,6 +460,7 @@ Add custom fields via `SECURENOW_SENSITIVE_FIELDS=field1,field2`.
460
460
  | `SECURENOW_API_KEY` | API key (`snk_live_...`); activates firewall when set | — |
461
461
  | `SECURENOW_API_URL` | SecureNow API base URL | `https://api.securenow.ai` |
462
462
  | `SECURENOW_FIREWALL_ENABLED` | Master kill-switch (`0` to disable) | `1` |
463
+ | `SECURENOW_FIREWALL_VERSION_INTERVAL` | Seconds between lightweight version checks | `10` |
463
464
  | `SECURENOW_FIREWALL_SYNC_INTERVAL` | Full blocklist refresh interval in seconds | `300` |
464
465
  | `SECURENOW_FIREWALL_FAIL_MODE` | `open` (allow all when unavailable) or `closed` | `open` |
465
466
  | `SECURENOW_FIREWALL_STATUS_CODE` | HTTP status for blocked requests | `403` |
@@ -470,6 +471,8 @@ Add custom fields via `SECURENOW_SENSITIVE_FIELDS=field1,field2`.
470
471
  | `SECURENOW_FIREWALL_CLOUD_DRY_RUN` | `1` to log cloud pushes without applying | `0` |
471
472
  | `SECURENOW_TRUSTED_PROXIES` | Comma-separated trusted proxy IPs | — |
472
473
 
474
+ **Resilience:** The firewall SDK includes a circuit breaker (opens after 5 consecutive errors, 2-min cooldown), in-flight request guards (prevents overlapping requests), 429 Retry-After support, and exponential backoff on both version checks and initial sync retries.
475
+
473
476
  ### Cloud WAF Provider Variables
474
477
 
475
478
  | Provider | Variables |
package/cli/config.js CHANGED
@@ -62,12 +62,18 @@ function getAuthSource() {
62
62
  function loadConfig() {
63
63
  const global = loadJSON(CONFIG_FILE);
64
64
  const local = fs.existsSync(LOCAL_CONFIG_FILE) ? loadJSON(LOCAL_CONFIG_FILE) : {};
65
+ if (hasLocalCredentials()) {
66
+ const { defaultApp: _ignored, ...globalWithoutAccountScoped } = global;
67
+ return { ...DEFAULTS, ...globalWithoutAccountScoped, ...local };
68
+ }
65
69
  return { ...DEFAULTS, ...global, ...local };
66
70
  }
67
71
 
68
- function saveConfig(config) {
69
- const existing = loadJSON(CONFIG_FILE);
70
- saveJSON(CONFIG_FILE, { ...existing, ...config });
72
+ function saveConfig(config, { local } = {}) {
73
+ const useLocal = local === true || (local == null && hasLocalCredentials());
74
+ const targetFile = useLocal ? LOCAL_CONFIG_FILE : CONFIG_FILE;
75
+ const existing = loadJSON(targetFile);
76
+ saveJSON(targetFile, { ...existing, ...config });
71
77
  }
72
78
 
73
79
  function getConfigValue(key) {
package/cli/monitor.js CHANGED
@@ -163,6 +163,24 @@ async function tracesAnalyze(args, flags) {
163
163
 
164
164
  // ── Logs ──
165
165
 
166
+ // Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
167
+ // Returns null on unparseable input so the caller can error out clearly
168
+ // instead of silently falling through to a default.
169
+ function parseDurationToMinutes(value) {
170
+ if (value == null || value === '') return null;
171
+ const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
172
+ if (!m) return null;
173
+ const n = parseInt(m[1], 10);
174
+ const unit = (m[2] || 'm').toLowerCase();
175
+ if (unit === 's') return Math.max(1, Math.round(n / 60));
176
+ if (unit === 'm') return n;
177
+ if (unit === 'h') return n * 60;
178
+ if (unit === 'd') return n * 60 * 24;
179
+ return null;
180
+ }
181
+
182
+ const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
183
+
166
184
  async function logsList(args, flags) {
167
185
  requireAuth();
168
186
  const appKey = resolveApp(flags);
@@ -171,17 +189,42 @@ async function logsList(args, flags) {
171
189
  process.exit(1);
172
190
  }
173
191
 
192
+ let minutes = 60;
193
+ if (flags.since != null) {
194
+ const parsed = parseDurationToMinutes(flags.since);
195
+ if (parsed == null) {
196
+ ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
197
+ process.exit(1);
198
+ }
199
+ minutes = parsed;
200
+ } else if (flags.minutes != null) {
201
+ const n = parseInt(flags.minutes, 10);
202
+ if (isNaN(n) || n <= 0) {
203
+ ui.error(`Invalid --minutes value "${flags.minutes}".`);
204
+ process.exit(1);
205
+ }
206
+ minutes = n;
207
+ }
208
+
209
+ let severity = null;
210
+ if (flags.level) {
211
+ severity = String(flags.level).toUpperCase();
212
+ if (!LOG_LEVELS.includes(severity)) {
213
+ ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
214
+ process.exit(1);
215
+ }
216
+ }
217
+
174
218
  const s = ui.spinner('Fetching logs');
175
219
  try {
176
- const minutes = parseInt(flags.minutes || '60', 10);
177
220
  const now = Date.now();
178
221
  const query = {
179
222
  appKeys: appKey,
180
- limit: flags.limit || 50,
223
+ limit: flags.limit || 200,
181
224
  from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
182
225
  to: flags.end || new Date(now).toISOString(),
183
226
  };
184
- if (flags.level) query.severity = flags.level;
227
+ if (severity) query.severity = severity;
185
228
 
186
229
  const data = await api.get('/logs', { query });
187
230
  const logs = data.logs || [];
@@ -193,11 +236,12 @@ async function logsList(args, flags) {
193
236
  for (const log of logs) {
194
237
  const level = (log.severityText || log.level || 'INFO').toUpperCase();
195
238
  const levelColor = {
196
- ERROR: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
197
- INFO: ui.c.cyan, DEBUG: ui.c.dim,
239
+ ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
240
+ INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
198
241
  }[level] || ui.c.white;
199
242
 
200
- const time = ui.c.dim(log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : '');
243
+ const parsedTime = ui.parseTimestamp(log.timestamp);
244
+ const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
201
245
  const body = log.body || log.message || log.severityText || '';
202
246
 
203
247
  console.log(` ${time} ${levelColor(level.padEnd(7))} ${body}`);
@@ -233,11 +277,12 @@ async function logsTrace(args, flags) {
233
277
  for (const log of logs) {
234
278
  const level = (log.severityText || log.level || 'INFO').toUpperCase();
235
279
  const levelColor = {
236
- ERROR: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
237
- INFO: ui.c.cyan, DEBUG: ui.c.dim,
280
+ ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
281
+ INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
238
282
  }[level] || ui.c.white;
239
283
 
240
- const time = ui.c.dim(log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : '');
284
+ const parsedTime = ui.parseTimestamp(log.timestamp);
285
+ const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
241
286
  console.log(` ${time} ${levelColor(level.padEnd(7))} ${log.body || log.message || ''}`);
242
287
  }
243
288
  console.log('');
package/cli/ui.js CHANGED
@@ -266,9 +266,36 @@ function hr() {
266
266
  console.log(c.dim('─'.repeat(width)));
267
267
  }
268
268
 
269
+ // Parses timestamps the backend returns in multiple formats:
270
+ // - UInt64 nanoseconds as a string (signoz logs) → "1775856777891000000"
271
+ // - ClickHouse DateTime64 string (signoz traces) → "2026-04-10 21:34:51.133000000" (UTC, no Z)
272
+ // - ISO 8601 → "2026-04-10T21:34:51.133Z"
273
+ // - number (ms) / Date → passthrough
274
+ function parseTimestamp(ts) {
275
+ if (ts == null || ts === '') return null;
276
+ if (ts instanceof Date) return isNaN(ts.getTime()) ? null : ts;
277
+ if (typeof ts === 'number') {
278
+ const d = new Date(ts);
279
+ return isNaN(d.getTime()) ? null : d;
280
+ }
281
+ const s = String(ts).trim();
282
+ if (/^\d{16,}$/.test(s)) {
283
+ const d = new Date(Number(s) / 1e6);
284
+ return isNaN(d.getTime()) ? null : d;
285
+ }
286
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(s) && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) {
287
+ const isoish = s.replace(' ', 'T').replace(/\.(\d{3})\d*$/, '.$1') + 'Z';
288
+ const d = new Date(isoish);
289
+ if (!isNaN(d.getTime())) return d;
290
+ }
291
+ const d = new Date(s);
292
+ return isNaN(d.getTime()) ? null : d;
293
+ }
294
+
269
295
  function timeAgo(dateStr) {
270
- if (!dateStr) return '—';
271
- const diff = Date.now() - new Date(dateStr).getTime();
296
+ const date = parseTimestamp(dateStr);
297
+ if (!date) return '—';
298
+ const diff = Date.now() - date.getTime();
272
299
  const seconds = Math.floor(diff / 1000);
273
300
  if (seconds < 60) return 'just now';
274
301
  const minutes = Math.floor(seconds / 60);
@@ -277,7 +304,7 @@ function timeAgo(dateStr) {
277
304
  if (hours < 24) return `${hours}h ago`;
278
305
  const days = Math.floor(hours / 24);
279
306
  if (days < 30) return `${days}d ago`;
280
- return new Date(dateStr).toLocaleDateString();
307
+ return date.toLocaleDateString();
281
308
  }
282
309
 
283
310
  function truncate(str, len = 50) {
@@ -351,6 +378,7 @@ module.exports = {
351
378
  json,
352
379
  hr,
353
380
  timeAgo,
381
+ parseTimestamp,
354
382
  truncate,
355
383
  statusBadge,
356
384
  httpStatusColor,
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');
@@ -90,7 +90,7 @@ const COMMANDS = {
90
90
  desc: 'View application logs',
91
91
  usage: 'securenow logs [options]',
92
92
  sub: {
93
- list: { desc: 'List recent logs', flags: { app: 'App key', limit: 'Max results', minutes: 'Time window in minutes', level: 'Filter by level' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
93
+ list: { desc: 'List recent logs', flags: { app: 'App key', limit: 'Max results (default 200)', since: 'Time window (e.g. 30m, 6h, 2d)', minutes: 'Time window in minutes (alias for --since Nm)', level: 'Filter by level (error, warn, info, debug)', start: 'Start time (ISO 8601)', end: 'End time (ISO 8601)' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
94
94
  trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
95
95
  },
96
96
  defaultSub: 'list',
@@ -345,6 +345,21 @@ The firewall uses the same trusted-proxy-aware IP resolution as SecureNow tracin
345
345
  - Store it in environment variables or `.env` — never commit it to source control
346
346
  - The key is hashed (SHA-256) on the server — SecureNow never stores the plaintext
347
347
 
348
+ ### Sync Architecture
349
+
350
+ The SDK uses a unified sync endpoint (`/firewall/sync`) that combines version checking and data fetching into a single request:
351
+
352
+ 1. **One request per poll** — The SDK sends its current blocklist and allowlist versions. The API responds with version info and only includes full IP lists for lists that have changed.
353
+ 2. **HTTP Keep-Alive** — TCP connections are reused across polls. TLS handshake happens once; subsequent requests reuse the socket (~5-10ms vs ~100-200ms per request).
354
+ 3. **Automatic fallback** — If the unified endpoint is not available (older API), the SDK falls back to legacy separate endpoints.
355
+
356
+ ### Circuit Breaker & Back-Pressure
357
+
358
+ - **Circuit breaker** — After 5 consecutive errors, the SDK pauses all polling for 2 minutes. After cooldown, a single probe is sent. If it succeeds, normal polling resumes.
359
+ - **In-flight guard** — Only one poll can be in-flight at a time. If the API is slow, the next scheduled poll is skipped.
360
+ - **429 Retry-After** — When the API returns HTTP 429, all polling pauses for the `Retry-After` duration.
361
+ - **Exponential backoff** — Poll interval doubles on each consecutive error (10s → 20s → 40s → 80s, capped at 120s).
362
+
348
363
  ### Cleanup on Shutdown
349
364
 
350
365
  All layers clean up on process exit (SIGINT/SIGTERM):
package/firewall.js CHANGED
@@ -8,7 +8,7 @@ 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
+ let _pollTimer = null;
12
12
  let _lastModified = null;
13
13
  let _lastVersion = null;
14
14
  let _lastSyncEtag = null;
@@ -25,36 +25,29 @@ let _allowlistRawIps = [];
25
25
  let _lastAllowlistModified = null;
26
26
  let _lastAllowlistVersion = null;
27
27
  let _lastAllowlistSyncEtag = null;
28
- let _allowlistVersionTimer = null;
29
- let _allowlistConsecutiveErrors = 0;
30
28
 
31
- // Circuit breaker: stops all outbound polling when the API is persistently down.
32
- // Opens after CIRCUIT_OPEN_THRESHOLD consecutive errors across both lists,
33
- // stays open for CIRCUIT_OPEN_COOLDOWN_MS, then half-opens (single probe).
29
+ // Circuit breaker
34
30
  const CIRCUIT_OPEN_THRESHOLD = 5;
35
31
  const CIRCUIT_OPEN_COOLDOWN_MS = 120_000;
36
- let _circuitState = 'closed'; // 'closed' | 'open' | 'half-open'
32
+ let _circuitState = 'closed';
37
33
  let _circuitOpenedAt = 0;
38
34
 
39
- // In-flight guards prevent overlapping requests when the API is slow
40
- let _versionCheckInflight = false;
41
- let _allowlistVersionCheckInflight = false;
42
- let _blocklistSyncInflight = false;
43
- let _allowlistSyncInflight = false;
44
-
45
- // 429 global back-off: if the API returns 429 with Retry-After, all polling
46
- // pauses until this timestamp.
35
+ // In-flight guard and 429 back-off
36
+ let _pollInflight = false;
47
37
  let _retryAfterUntil = 0;
48
38
 
49
- // ────── Circuit Breaker ──────
39
+ // Keep-alive agents reuse TCP connections across polls (TLS handshake once)
40
+ const _httpAgent = new http.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 30_000 });
41
+ const _httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 30_000 });
50
42
 
51
- function totalConsecutiveErrors() {
52
- return _consecutiveErrors + _allowlistConsecutiveErrors;
53
- }
43
+ // Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
44
+ let _useUnifiedSync = true;
45
+
46
+ // ────── Circuit Breaker ──────
54
47
 
55
48
  function maybeOpenCircuit() {
56
49
  if (_circuitState === 'open') return;
57
- if (totalConsecutiveErrors() >= CIRCUIT_OPEN_THRESHOLD) {
50
+ if (_consecutiveErrors >= CIRCUIT_OPEN_THRESHOLD) {
58
51
  _circuitState = 'open';
59
52
  _circuitOpenedAt = Date.now();
60
53
  if (_options && _options.log) {
@@ -69,7 +62,6 @@ function resetCircuit() {
69
62
  if (_options && _options.log) console.log('[securenow] Firewall: circuit breaker CLOSED — API healthy');
70
63
  }
71
64
  _consecutiveErrors = 0;
72
- _allowlistConsecutiveErrors = 0;
73
65
  }
74
66
 
75
67
  function shouldSkipRequest() {
@@ -97,23 +89,25 @@ function handleRetryAfter(res) {
97
89
  }
98
90
  }
99
91
 
100
- // ────── Blocklist Sync ──────
92
+ // ────── HTTP helpers ──────
101
93
 
102
94
  function buildUrl(apiUrl, path) {
103
95
  return apiUrl.replace(/\/+$/, '') + '/api/v1' + path;
104
96
  }
105
97
 
106
- function syncBlocklist(callback) {
107
- if (_blocklistSyncInflight) { callback(null, false); return; }
108
- _blocklistSyncInflight = true;
98
+ function jitter(baseMs) {
99
+ return baseMs * (0.8 + Math.random() * 0.4);
100
+ }
109
101
 
110
- const done = (...args) => { _blocklistSyncInflight = false; callback(...args); };
102
+ function agentFor(url) {
103
+ return url.startsWith('https') ? _httpsAgent : _httpAgent;
104
+ }
111
105
 
112
- const url = buildUrl(_options.apiUrl, '/firewall/blocklist');
106
+ function httpGet(url, extraHeaders, timeout, callback) {
113
107
  const mod = url.startsWith('https') ? https : http;
114
108
  const parsed = new URL(url);
115
109
 
116
- const reqOptions = {
110
+ const req = mod.request({
117
111
  hostname: parsed.hostname,
118
112
  port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
119
113
  path: parsed.pathname + parsed.search,
@@ -121,297 +115,315 @@ function syncBlocklist(callback) {
121
115
  headers: {
122
116
  'Authorization': `Bearer ${_options.apiKey}`,
123
117
  'User-Agent': 'securenow-firewall-sdk',
118
+ ...extraHeaders,
124
119
  },
125
- timeout: 10000,
126
- };
127
-
128
- if (_lastSyncEtag) {
129
- reqOptions.headers['If-None-Match'] = _lastSyncEtag;
130
- } else if (_lastModified) {
131
- reqOptions.headers['If-Modified-Since'] = _lastModified;
132
- }
133
-
134
- const req = mod.request(reqOptions, (res) => {
135
- if (res.statusCode === 304) {
136
- done(null, false);
137
- return;
138
- }
139
- if (res.statusCode === 429) { handleRetryAfter(res); }
140
-
120
+ timeout,
121
+ agent: agentFor(url),
122
+ }, (res) => {
141
123
  let data = '';
142
124
  res.on('data', (chunk) => { data += chunk; });
143
- res.on('end', () => {
144
- if (res.statusCode !== 200) {
145
- done(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
146
- return;
147
- }
148
- try {
149
- const body = JSON.parse(data);
150
- const ips = body.ips || [];
151
- _rawIps = ips;
152
- _matcher = createMatcher(ips);
153
- _lastModified = res.headers['last-modified'] || null;
154
- if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
155
- _stats.syncs++;
156
- notifyLayers(ips);
157
- done(null, true, _matcher.stats());
158
- } catch (e) {
159
- done(new Error(`Failed to parse blocklist response: ${e.message}`));
160
- }
161
- });
125
+ res.on('end', () => { callback(null, res, data); });
162
126
  });
163
127
 
164
- req.on('error', (err) => done(err));
165
- req.on('timeout', () => { req.destroy(); done(new Error('Sync request timed out')); });
128
+ req.on('error', (err) => callback(err));
129
+ req.on('timeout', () => { req.destroy(); callback(new Error('Request timed out')); });
166
130
  req.end();
167
131
  }
168
132
 
169
- function notifyLayers(ips) {
170
- for (const layer of _layers) {
171
- try { if (typeof layer.sync === 'function') layer.sync(ips); } catch (_) {}
172
- }
173
- }
133
+ // ────── Unified Sync (v2 — single request for everything) ──────
174
134
 
175
- // ────── Allowlist Sync ──────
135
+ function doUnifiedSync(callback) {
136
+ const url = buildUrl(_options.apiUrl, '/firewall/sync');
137
+ const headers = {};
138
+ if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
139
+ if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
176
140
 
177
- function syncAllowlist(callback) {
178
- if (_allowlistSyncInflight) { callback(null, false); return; }
179
- _allowlistSyncInflight = true;
141
+ httpGet(url, headers, 8000, (err, res, data) => {
142
+ if (err) return callback(err);
180
143
 
181
- const done = (...args) => { _allowlistSyncInflight = false; callback(...args); };
144
+ _stats.versionChecks++;
182
145
 
183
- const url = buildUrl(_options.apiUrl, '/firewall/allowlist');
184
- const mod = url.startsWith('https') ? https : http;
185
- const parsed = new URL(url);
146
+ if (res.statusCode === 304) {
147
+ return callback(null, { blChanged: false, alChanged: false });
148
+ }
186
149
 
187
- const reqOptions = {
188
- hostname: parsed.hostname,
189
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
190
- path: parsed.pathname + parsed.search,
191
- method: 'GET',
192
- headers: {
193
- 'Authorization': `Bearer ${_options.apiKey}`,
194
- 'User-Agent': 'securenow-firewall-sdk',
195
- },
196
- timeout: 10000,
197
- };
150
+ if (res.statusCode === 404) {
151
+ _useUnifiedSync = false;
152
+ if (_options.log) console.log('[securenow] Firewall: /sync not available, using legacy endpoints');
153
+ return callback(null, { blChanged: false, alChanged: false, useLegacy: true });
154
+ }
198
155
 
199
- if (_lastAllowlistSyncEtag) {
200
- reqOptions.headers['If-None-Match'] = _lastAllowlistSyncEtag;
201
- } else if (_lastAllowlistModified) {
202
- reqOptions.headers['If-Modified-Since'] = _lastAllowlistModified;
203
- }
156
+ if (res.statusCode === 429) { handleRetryAfter(res); }
204
157
 
205
- const req = mod.request(reqOptions, (res) => {
206
- if (res.statusCode === 304) {
207
- done(null, false);
208
- return;
158
+ if (res.statusCode !== 200) {
159
+ return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
209
160
  }
210
- if (res.statusCode === 429) { handleRetryAfter(res); }
211
161
 
212
- let data = '';
213
- res.on('data', (chunk) => { data += chunk; });
214
- res.on('end', () => {
215
- if (res.statusCode !== 200) {
216
- done(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
217
- return;
162
+ try {
163
+ const body = JSON.parse(data);
164
+
165
+ let blChanged = false;
166
+ let alChanged = false;
167
+
168
+ // Update blocklist version + data
169
+ if (body.blocklist) {
170
+ const newVer = body.blocklist.version;
171
+ if (newVer !== _lastVersion) {
172
+ _lastVersion = newVer;
173
+ blChanged = true;
174
+ }
218
175
  }
219
- try {
220
- const body = JSON.parse(data);
221
- const ips = body.ips || [];
222
- _allowlistRawIps = ips;
223
- _allowlistMatcher = createMatcher(ips);
224
- _lastAllowlistModified = res.headers['last-modified'] || null;
225
- if (res.headers['etag']) _lastAllowlistSyncEtag = res.headers['etag'];
226
- done(null, true, _allowlistMatcher.stats());
227
- } catch (e) {
228
- done(new Error(`Failed to parse allowlist response: ${e.message}`));
176
+
177
+ if (body.blocklistIps) {
178
+ _rawIps = body.blocklistIps;
179
+ _matcher = createMatcher(body.blocklistIps);
180
+ _stats.syncs++;
181
+ notifyLayers(body.blocklistIps);
182
+ blChanged = true;
229
183
  }
230
- });
231
- });
232
184
 
233
- req.on('error', (err) => done(err));
234
- req.on('timeout', () => { req.destroy(); done(new Error('Allowlist sync request timed out')); });
235
- req.end();
236
- }
185
+ // Update allowlist version + data
186
+ if (body.allowlist) {
187
+ const newVer = body.allowlist.version;
188
+ if (newVer !== _lastAllowlistVersion) {
189
+ _lastAllowlistVersion = newVer;
190
+ alChanged = true;
191
+ }
192
+ }
237
193
 
238
- function checkAllowlistVersion(callback) {
239
- if (_allowlistVersionCheckInflight || shouldSkipRequest()) { callback(null, false); return; }
240
- _allowlistVersionCheckInflight = true;
194
+ if (body.allowlistIps) {
195
+ _allowlistRawIps = body.allowlistIps;
196
+ _allowlistMatcher = createMatcher(body.allowlistIps);
197
+ alChanged = true;
198
+ }
241
199
 
242
- const done = (...args) => { _allowlistVersionCheckInflight = false; callback(...args); };
200
+ callback(null, { blChanged, alChanged });
201
+ } catch (e) {
202
+ callback(new Error(`Failed to parse sync response: ${e.message}`));
203
+ }
204
+ });
205
+ }
243
206
 
244
- const url = buildUrl(_options.apiUrl, '/firewall/allowlist/version');
245
- const mod = url.startsWith('https') ? https : http;
246
- const parsed = new URL(url);
207
+ // ────── Legacy Sync (v1 — separate endpoints, kept for backward compat) ──────
247
208
 
248
- const headers = {
249
- 'Authorization': `Bearer ${_options.apiKey}`,
250
- 'User-Agent': 'securenow-firewall-sdk',
251
- };
252
- if (_lastAllowlistVersion) headers['If-None-Match'] = _lastAllowlistVersion;
209
+ function legacyBlocklistSync(callback) {
210
+ const url = buildUrl(_options.apiUrl, '/firewall/blocklist');
211
+ const headers = {};
212
+ if (_lastSyncEtag) headers['If-None-Match'] = _lastSyncEtag;
213
+ else if (_lastModified) headers['If-Modified-Since'] = _lastModified;
253
214
 
254
- const req = mod.request({
255
- hostname: parsed.hostname,
256
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
257
- path: parsed.pathname + parsed.search,
258
- method: 'GET',
259
- headers,
260
- timeout: 5000,
261
- }, (res) => {
262
- if (res.statusCode === 304) {
263
- _allowlistConsecutiveErrors = 0;
264
- resetCircuit();
265
- res.resume();
266
- done(null, false);
267
- return;
268
- }
215
+ httpGet(url, headers, 10000, (err, res, data) => {
216
+ if (err) return callback(err);
217
+ if (res.statusCode === 304) return callback(null, false);
269
218
  if (res.statusCode === 429) { handleRetryAfter(res); }
219
+ if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
270
220
 
271
- let data = '';
272
- res.on('data', (chunk) => { data += chunk; });
273
- res.on('end', () => {
274
- if (res.statusCode !== 200) {
275
- _allowlistConsecutiveErrors++;
276
- _stats.errors++;
277
- maybeOpenCircuit();
278
- done(null, false);
279
- return;
280
- }
281
- _allowlistConsecutiveErrors = 0;
282
- resetCircuit();
283
- try {
284
- const body = JSON.parse(data);
285
- const version = body.version || null;
286
- const changed = version !== _lastAllowlistVersion;
287
- if (changed) _lastAllowlistVersion = version;
288
- done(null, changed);
289
- } catch (_e) { done(null, false); }
290
- });
221
+ try {
222
+ const body = JSON.parse(data);
223
+ const ips = body.ips || [];
224
+ _rawIps = ips;
225
+ _matcher = createMatcher(ips);
226
+ _lastModified = res.headers['last-modified'] || null;
227
+ if (res.headers['etag']) _lastSyncEtag = res.headers['etag'];
228
+ _stats.syncs++;
229
+ notifyLayers(ips);
230
+ callback(null, true, _matcher.stats());
231
+ } catch (e) {
232
+ callback(new Error(`Failed to parse blocklist: ${e.message}`));
233
+ }
291
234
  });
292
-
293
- req.on('error', () => { _allowlistConsecutiveErrors++; _stats.errors++; maybeOpenCircuit(); done(null, false); });
294
- req.on('timeout', () => { req.destroy(); _allowlistConsecutiveErrors++; _stats.errors++; maybeOpenCircuit(); done(null, false); });
295
- req.end();
296
235
  }
297
236
 
298
- function doFullAllowlistSync() {
299
- syncAllowlist((err, changed, stats) => {
300
- if (err) {
301
- if (_options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err.message);
302
- } else if (changed && stats && _options.log) {
303
- console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
237
+ function legacyAllowlistSync(callback) {
238
+ const url = buildUrl(_options.apiUrl, '/firewall/allowlist');
239
+ const headers = {};
240
+ if (_lastAllowlistSyncEtag) headers['If-None-Match'] = _lastAllowlistSyncEtag;
241
+ else if (_lastAllowlistModified) headers['If-Modified-Since'] = _lastAllowlistModified;
242
+
243
+ httpGet(url, headers, 10000, (err, res, data) => {
244
+ if (err) return callback(err);
245
+ if (res.statusCode === 304) return callback(null, false);
246
+ if (res.statusCode === 429) { handleRetryAfter(res); }
247
+ if (res.statusCode !== 200) return callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
248
+
249
+ try {
250
+ const body = JSON.parse(data);
251
+ const ips = body.ips || [];
252
+ _allowlistRawIps = ips;
253
+ _allowlistMatcher = createMatcher(ips);
254
+ _lastAllowlistModified = res.headers['last-modified'] || null;
255
+ if (res.headers['etag']) _lastAllowlistSyncEtag = res.headers['etag'];
256
+ callback(null, true, _allowlistMatcher.stats());
257
+ } catch (e) {
258
+ callback(new Error(`Failed to parse allowlist: ${e.message}`));
304
259
  }
305
260
  });
306
261
  }
307
262
 
308
- function scheduleNextAllowlistVersionCheck() {
309
- const baseMs = (_options.versionCheckInterval || 10) * 1000;
310
- const backoffMs = Math.min(baseMs * Math.pow(2, _allowlistConsecutiveErrors), 120_000);
311
- const delayMs = jitter(backoffMs);
263
+ function legacyVersionCheck(callback) {
264
+ const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
265
+ const headers = {};
266
+ if (_lastVersion) headers['If-None-Match'] = _lastVersion;
312
267
 
313
- _allowlistVersionTimer = setTimeout(() => {
314
- checkAllowlistVersion((_err, changed) => {
315
- if (changed) {
316
- if (_options.log) console.log('[securenow] Firewall: allowlist version changed, syncing…');
317
- doFullAllowlistSync();
318
- }
319
- scheduleNextAllowlistVersionCheck();
320
- });
321
- }, delayMs);
322
- if (_allowlistVersionTimer.unref) _allowlistVersionTimer.unref();
268
+ httpGet(url, headers, 5000, (err, res, data) => {
269
+ if (err) return callback(err);
270
+ _stats.versionChecks++;
271
+ if (res.statusCode === 304) return callback(null, false, false);
272
+ if (res.statusCode === 429) { handleRetryAfter(res); }
273
+ if (res.statusCode !== 200) return callback(new Error(`API ${res.statusCode}`));
274
+
275
+ try {
276
+ const body = JSON.parse(data);
277
+ const version = body.version || null;
278
+ const changed = version !== _lastVersion;
279
+ if (changed) _lastVersion = version;
280
+ callback(null, changed, false);
281
+ } catch (_e) { callback(null, false, false); }
282
+ });
323
283
  }
324
284
 
325
- function checkVersion(callback) {
326
- const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
327
- const mod = url.startsWith('https') ? https : http;
328
- const parsed = new URL(url);
285
+ function legacyAllowlistVersionCheck(callback) {
286
+ const url = buildUrl(_options.apiUrl, '/firewall/allowlist/version');
287
+ const headers = {};
288
+ if (_lastAllowlistVersion) headers['If-None-Match'] = _lastAllowlistVersion;
329
289
 
330
- const headers = {
331
- 'Authorization': `Bearer ${_options.apiKey}`,
332
- 'User-Agent': 'securenow-firewall-sdk',
333
- };
334
- if (_lastVersion) headers['If-None-Match'] = _lastVersion;
290
+ httpGet(url, headers, 5000, (err, res, data) => {
291
+ if (err) return callback(err);
292
+ if (res.statusCode === 304) return callback(null, false);
293
+ if (res.statusCode === 429) { handleRetryAfter(res); }
294
+ if (res.statusCode !== 200) return callback(new Error(`API ${res.statusCode}`));
335
295
 
336
- const req = mod.request({
337
- hostname: parsed.hostname,
338
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
339
- path: parsed.pathname + parsed.search,
340
- method: 'GET',
341
- headers,
342
- timeout: 5000,
343
- }, (res) => {
344
- _stats.versionChecks++;
296
+ try {
297
+ const body = JSON.parse(data);
298
+ const version = body.version || null;
299
+ const changed = version !== _lastAllowlistVersion;
300
+ if (changed) _lastAllowlistVersion = version;
301
+ callback(null, changed);
302
+ } catch (_e) { callback(null, false); }
303
+ });
304
+ }
345
305
 
346
- if (res.statusCode === 304) {
347
- _consecutiveErrors = 0;
348
- res.resume();
349
- callback(null, false);
350
- return;
306
+ function doLegacyPoll(callback) {
307
+ let pending = 2;
308
+ let blChanged = false;
309
+ let alChanged = false;
310
+ let firstError = null;
311
+
312
+ function checkDone() {
313
+ if (--pending > 0) return;
314
+ if (firstError) return callback(firstError);
315
+
316
+ let syncsPending = 0;
317
+ if (blChanged) syncsPending++;
318
+ if (alChanged) syncsPending++;
319
+ if (syncsPending === 0) return callback(null, { blChanged: false, alChanged: false });
320
+
321
+ function syncDone() {
322
+ if (--syncsPending > 0) return;
323
+ callback(null, { blChanged, alChanged });
351
324
  }
352
325
 
353
- let data = '';
354
- res.on('data', (chunk) => { data += chunk; });
355
- res.on('end', () => {
356
- if (res.statusCode !== 200) {
357
- _consecutiveErrors++;
358
- _stats.errors++;
359
- callback(null, false);
360
- return;
361
- }
362
- _consecutiveErrors = 0;
363
- try {
364
- const body = JSON.parse(data);
365
- const version = body.version || null;
366
- const changed = version !== _lastVersion;
367
- if (changed) _lastVersion = version;
368
- callback(null, changed);
369
- } catch (_e) { callback(null, false); }
370
- });
326
+ if (blChanged) {
327
+ legacyBlocklistSync((err, changed, stats) => {
328
+ if (err && _options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
329
+ else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
330
+ syncDone();
331
+ });
332
+ }
333
+ if (alChanged) {
334
+ legacyAllowlistSync((err, changed, stats) => {
335
+ if (err && _options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err.message);
336
+ else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
337
+ syncDone();
338
+ });
339
+ }
340
+ }
341
+
342
+ legacyVersionCheck((err, changed) => {
343
+ if (err) firstError = firstError || err;
344
+ blChanged = changed;
345
+ checkDone();
371
346
  });
372
347
 
373
- req.on('error', () => { _consecutiveErrors++; _stats.errors++; callback(null, false); });
374
- req.on('timeout', () => { req.destroy(); _consecutiveErrors++; _stats.errors++; callback(null, false); });
375
- req.end();
348
+ legacyAllowlistVersionCheck((err, changed) => {
349
+ if (err) firstError = firstError || err;
350
+ alChanged = changed;
351
+ checkDone();
352
+ });
353
+ }
354
+
355
+ // ────── Unified poll loop ──────
356
+
357
+ function notifyLayers(ips) {
358
+ for (const layer of _layers) {
359
+ try { if (typeof layer.sync === 'function') layer.sync(ips); } catch (_) {}
360
+ }
376
361
  }
377
362
 
378
- function doFullSync() {
379
- syncBlocklist((err, changed, stats) => {
363
+ function pollOnce(callback) {
364
+ if (_pollInflight || shouldSkipRequest()) return callback(null);
365
+ _pollInflight = true;
366
+
367
+ const done = (err, result) => {
368
+ _pollInflight = false;
380
369
  if (err) {
381
- if (_options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
382
- } else if (changed && stats && _options.log) {
383
- console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
370
+ _consecutiveErrors++;
371
+ _stats.errors++;
372
+ maybeOpenCircuit();
373
+ if (_options.log) console.warn('[securenow] Firewall: poll failed:', err.message);
374
+ return callback(err);
384
375
  }
385
- });
386
- }
376
+ _consecutiveErrors = 0;
377
+ resetCircuit();
378
+ if (result) {
379
+ if (result.blChanged && _options.log && _matcher) {
380
+ const s = _matcher.stats();
381
+ console.log('[securenow] Firewall: re-synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
382
+ }
383
+ if (result.alChanged && _options.log && _allowlistMatcher) {
384
+ const s = _allowlistMatcher.stats();
385
+ console.log('[securenow] Firewall: re-synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
386
+ }
387
+ }
388
+ callback(null);
389
+ };
387
390
 
388
- function jitter(baseMs) {
389
- return baseMs * (0.8 + Math.random() * 0.4);
391
+ if (_useUnifiedSync) {
392
+ doUnifiedSync((err, result) => {
393
+ if (err) return done(err);
394
+ if (result && result.useLegacy) {
395
+ doLegacyPoll(done);
396
+ } else {
397
+ done(null, result);
398
+ }
399
+ });
400
+ } else {
401
+ doLegacyPoll(done);
402
+ }
390
403
  }
391
404
 
392
- function scheduleNextVersionCheck() {
405
+ function scheduleNextPoll() {
393
406
  const baseMs = (_options.versionCheckInterval || 10) * 1000;
394
407
  const backoffMs = Math.min(baseMs * Math.pow(2, _consecutiveErrors), 120_000);
395
408
  const delayMs = jitter(backoffMs);
396
409
 
397
- _versionTimer = setTimeout(() => {
398
- checkVersion((_err, changed) => {
399
- if (changed) {
400
- if (_options.log) console.log('[securenow] Firewall: blocklist version changed, syncing…');
401
- doFullSync();
402
- }
403
- scheduleNextVersionCheck();
404
- });
410
+ _pollTimer = setTimeout(() => {
411
+ pollOnce(() => { scheduleNextPoll(); });
405
412
  }, delayMs);
406
- if (_versionTimer.unref) _versionTimer.unref();
413
+ if (_pollTimer.unref) _pollTimer.unref();
407
414
  }
408
415
 
409
416
  function startSyncLoop() {
410
417
  const fullSyncIntervalMs = (_options.syncInterval || 300) * 1000;
411
- const RETRY_DELAY = 5000;
418
+ const BASE_RETRY = 5000;
419
+ const MAX_RETRY = 120_000;
420
+ let _initAttempt = 0;
412
421
 
413
422
  function initialSync() {
414
- syncBlocklist((err, changed, stats) => {
423
+ // Use unified endpoint for initial sync too
424
+ const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
425
+
426
+ syncFn((err, result) => {
415
427
  if (err) {
416
428
  const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
417
429
  if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
@@ -423,42 +435,58 @@ function startSyncLoop() {
423
435
  if (retryTimer.unref) retryTimer.unref();
424
436
  return;
425
437
  }
438
+ if (result && result.useLegacy) {
439
+ const retryTimer = setTimeout(initialSync, 1000);
440
+ if (retryTimer.unref) retryTimer.unref();
441
+ return;
442
+ }
426
443
  if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
427
444
  if (_options.failMode === 'closed') {
428
445
  _matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
429
446
  }
430
- const retryTimer = setTimeout(initialSync, RETRY_DELAY);
447
+ _initAttempt++;
448
+ const delay = Math.min(BASE_RETRY * Math.pow(2, _initAttempt), MAX_RETRY);
449
+ const retryTimer = setTimeout(initialSync, jitter(delay));
431
450
  if (retryTimer.unref) retryTimer.unref();
432
451
  return;
433
452
  }
434
- if (changed && stats) {
435
- if (_options.log) console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
436
- }
437
- _initialized = true;
438
- });
439
- }
440
453
 
441
- function initialAllowlistSync() {
442
- syncAllowlist((err, changed, stats) => {
443
- if (err) {
444
- if (_options.log) console.warn('[securenow] Firewall: initial allowlist sync failed:', err.message);
445
- const retryTimer = setTimeout(initialAllowlistSync, RETRY_DELAY);
454
+ if (result && result.useLegacy) {
455
+ const retryTimer = setTimeout(initialSync, 1000);
446
456
  if (retryTimer.unref) retryTimer.unref();
447
457
  return;
448
458
  }
449
- if (changed && stats && stats.total > 0) {
450
- if (_options.log) console.log('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
459
+
460
+ _initialized = true;
461
+ if (_options.log && _matcher) {
462
+ const s = _matcher.stats();
463
+ console.log('[securenow] Firewall: synced %d blocked IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
464
+ }
465
+ if (_options.log && _allowlistMatcher) {
466
+ const s = _allowlistMatcher.stats();
467
+ if (s.total > 0) console.log('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', s.total, s.exact, s.cidr);
451
468
  }
452
469
  });
453
470
  }
454
471
 
455
472
  initialSync();
456
- initialAllowlistSync();
457
-
458
- scheduleNextVersionCheck();
459
- scheduleNextAllowlistVersionCheck();
460
-
461
- _syncTimer = setInterval(() => { doFullSync(); doFullAllowlistSync(); }, fullSyncIntervalMs);
473
+ scheduleNextPoll();
474
+
475
+ // Safety-net full sync timer (less frequent, uses same path)
476
+ _syncTimer = setInterval(() => {
477
+ if (shouldSkipRequest()) return;
478
+ // Force a full re-fetch by clearing versions so unified endpoint returns full data
479
+ const savedBlVer = _lastVersion;
480
+ const savedAlVer = _lastAllowlistVersion;
481
+ _lastVersion = null;
482
+ _lastAllowlistVersion = null;
483
+ pollOnce((err) => {
484
+ if (err) {
485
+ _lastVersion = savedBlVer;
486
+ _lastAllowlistVersion = savedAlVer;
487
+ }
488
+ });
489
+ }, fullSyncIntervalMs);
462
490
  if (_syncTimer.unref) _syncTimer.unref();
463
491
  }
464
492
 
@@ -541,7 +569,6 @@ function firewallRequestHandler(req, res) {
541
569
  sendBlockResponse(req, res, ip);
542
570
  return true;
543
571
  }
544
- // IP is on the allowlist — skip blocklist check, allow through
545
572
  return false;
546
573
  }
547
574
 
@@ -575,7 +602,6 @@ function patchHttpLayer() {
575
602
  if (_httpPatched) return;
576
603
  _httpPatched = true;
577
604
 
578
- // Patch Server.prototype.emit to intercept requests on already-created servers
579
605
  patchEmitLayer();
580
606
 
581
607
  http.createServer = function(...args) {
@@ -602,11 +628,9 @@ function init(options) {
602
628
 
603
629
  if (_options.log) console.log('[securenow] Firewall: ENABLED');
604
630
 
605
- // Layer 1: HTTP (always on)
606
631
  patchHttpLayer();
607
632
  if (_options.log) console.log('[securenow] Firewall: Layer 1 (HTTP 403) active');
608
633
 
609
- // Layer 2: TCP
610
634
  if (_options.tcp) {
611
635
  try {
612
636
  const tcpLayer = require('./firewall-tcp');
@@ -620,7 +644,6 @@ function init(options) {
620
644
  if (_options.log) console.log('[securenow] Firewall: Layer 2 (TCP drop) disabled (set SECURENOW_FIREWALL_TCP=1)');
621
645
  }
622
646
 
623
- // Layer 3: iptables
624
647
  if (_options.iptables) {
625
648
  try {
626
649
  const iptablesLayer = require('./firewall-iptables');
@@ -634,7 +657,6 @@ function init(options) {
634
657
  if (_options.log) console.log('[securenow] Firewall: Layer 3 (iptables) disabled (set SECURENOW_FIREWALL_IPTABLES=1)');
635
658
  }
636
659
 
637
- // Layer 4: Cloud WAF
638
660
  if (_options.cloud) {
639
661
  try {
640
662
  const cloudLayer = require('./firewall-cloud');
@@ -652,10 +674,18 @@ function init(options) {
652
674
  }
653
675
 
654
676
  function shutdown() {
655
- if (_versionTimer) { clearTimeout(_versionTimer); _versionTimer = null; }
656
- if (_allowlistVersionTimer) { clearTimeout(_allowlistVersionTimer); _allowlistVersionTimer = null; }
677
+ if (_pollTimer) { clearTimeout(_pollTimer); _pollTimer = null; }
657
678
  if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
658
679
 
680
+ _circuitState = 'closed';
681
+ _circuitOpenedAt = 0;
682
+ _consecutiveErrors = 0;
683
+ _pollInflight = false;
684
+ _retryAfterUntil = 0;
685
+
686
+ _httpAgent.destroy();
687
+ _httpsAgent.destroy();
688
+
659
689
  for (const layer of _layers) {
660
690
  try { if (typeof layer.shutdown === 'function') layer.shutdown(); } catch (_) {}
661
691
  }
@@ -678,6 +708,9 @@ function getStats() {
678
708
  matcher: _matcher ? _matcher.stats() : null,
679
709
  allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
680
710
  initialized: _initialized,
711
+ circuitState: _circuitState,
712
+ consecutiveErrors: _consecutiveErrors,
713
+ unifiedSync: _useUnifiedSync,
681
714
  };
682
715
  }
683
716
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.16.3",
3
+ "version": "5.17.1",
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",