securenow 7.6.5 → 7.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/auth.js CHANGED
@@ -237,7 +237,7 @@ async function login(args, flags) {
237
237
  const email = payload?.email || 'unknown';
238
238
  const exp = payload?.exp ? payload.exp * 1000 : null;
239
239
 
240
- config.setAuth(token, email, exp, { local });
240
+ config.setAuth(token, email, exp, { local, enableFirewall: true });
241
241
  if (local) config.ensureLocalGitignore();
242
242
  console.log('');
243
243
  ui.success(`Logged in as ${ui.c.bold(email)}`);
@@ -255,7 +255,7 @@ async function login(args, flags) {
255
255
  const email = payload?.email || 'unknown';
256
256
  const exp = payload?.exp ? payload.exp * 1000 : null;
257
257
 
258
- config.setAuth(token, email, exp, { local, app });
258
+ config.setAuth(token, email, exp, { local, app, enableFirewall: true });
259
259
  if (apiKey) config.setApiKey(apiKey, { local });
260
260
  if (local) config.ensureLocalGitignore();
261
261
  console.log('');
package/cli/config.js CHANGED
@@ -101,11 +101,22 @@ function saveCredentials(creds, { local = false } = {}) {
101
101
  saveJSON(targetFile, appConfig.withCredentialDefaults(creds) || {});
102
102
  }
103
103
 
104
- function ensureCredentialDefaults({ local } = {}) {
104
+ function withOnboardingFirewallEnabled(creds) {
105
+ const payload = appConfig.withCredentialDefaults(creds || {}) || {};
106
+ payload.config = payload.config || {};
107
+ payload.config.firewall = payload.config.firewall || {};
108
+ payload.config.firewall.enabled = true;
109
+ return payload;
110
+ }
111
+
112
+ function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
105
113
  const useLocal = local === true || (local == null && hasLocalCredentials());
106
114
  const targetFile = credentialsFileForLocal(useLocal);
107
115
  const existing = loadJSON(targetFile);
108
- saveJSON(targetFile, appConfig.withCredentialDefaults(existing || {}) || {});
116
+ const payload = enableFirewall
117
+ ? withOnboardingFirewallEnabled(existing || {})
118
+ : appConfig.withCredentialDefaults(existing || {}) || {};
119
+ saveJSON(targetFile, payload);
109
120
  }
110
121
 
111
122
  function clearCredentials({ local } = {}) {
@@ -132,7 +143,7 @@ function getToken() {
132
143
  return creds.token;
133
144
  }
134
145
 
135
- function setAuth(token, email, expiresAt, { local = false, app = null } = {}) {
146
+ function setAuth(token, email, expiresAt, { local = false, app = null, enableFirewall = false } = {}) {
136
147
  const targetFile = credentialsFileForLocal(local);
137
148
  const payload = { ...loadJSON(targetFile), token, email, expiresAt };
138
149
  if (app && (app.key || app.name || app.instance)) {
@@ -142,7 +153,10 @@ function setAuth(token, email, expiresAt, { local = false, app = null } = {}) {
142
153
  instance: app.instance || null,
143
154
  };
144
155
  }
145
- saveCredentials(payload, { local });
156
+ saveJSON(
157
+ targetFile,
158
+ enableFirewall ? withOnboardingFirewallEnabled(payload) : appConfig.withCredentialDefaults(payload) || {}
159
+ );
146
160
  }
147
161
 
148
162
  function getApp() {
@@ -154,7 +168,7 @@ function setApiKey(apiKey, { local } = {}) {
154
168
  const useLocal = local === true || (local == null && hasLocalCredentials());
155
169
  const targetFile = credentialsFileForLocal(useLocal);
156
170
  const existing = loadJSON(targetFile);
157
- saveJSON(targetFile, appConfig.withCredentialDefaults({ ...existing, apiKey }) || { apiKey });
171
+ saveJSON(targetFile, withOnboardingFirewallEnabled({ ...existing, apiKey }));
158
172
  }
159
173
 
160
174
  function clearApiKey({ local } = {}) {
@@ -238,6 +252,7 @@ module.exports = {
238
252
  getAuthSource,
239
253
  hasLocalCredentials,
240
254
  ensureCredentialDefaults,
255
+ withOnboardingFirewallEnabled,
241
256
  ensureLocalGitignore,
242
257
  getApiUrl,
243
258
  getAppUrl,
@@ -20,7 +20,7 @@ function buildRuntimeCredentials(options = {}) {
20
20
  options.env ||
21
21
  appConfig.resolveDeploymentEnvironment() ||
22
22
  'production';
23
- const runtime = appConfig.withCredentialDefaults({
23
+ const runtime = config.withOnboardingFirewallEnabled({
24
24
  apiKey: creds.apiKey || null,
25
25
  app: {
26
26
  key: creds.app?.key || null,
@@ -33,6 +33,10 @@ function buildRuntimeCredentials(options = {}) {
33
33
  ...(creds.config?.runtime || {}),
34
34
  deploymentEnvironment,
35
35
  },
36
+ firewall: {
37
+ ...(creds.config?.firewall || {}),
38
+ enabled: true,
39
+ },
36
40
  },
37
41
  _securenow: {
38
42
  ...(creds._securenow || {}),
package/cli/init.js CHANGED
@@ -86,7 +86,7 @@ async function init(_args, flags) {
86
86
  }
87
87
 
88
88
  function initCredentials(flags) {
89
- config.ensureCredentialDefaults({ local: true });
89
+ config.ensureCredentialDefaults({ local: true, enableFirewall: true });
90
90
  config.ensureLocalGitignore();
91
91
  const creds = config.loadCredentials();
92
92
  creds.config = creds.config || {};
package/firewall.js CHANGED
@@ -45,12 +45,15 @@ let _circuitOpenedAt = 0;
45
45
  let _pollInflight = false;
46
46
  let _retryAfterUntil = 0;
47
47
 
48
- // Keep-alive agents reuse TCP connections across polls (TLS handshake once)
49
- const _httpAgent = new http.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 30_000 });
50
- const _httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 2, keepAliveMsecs: 30_000 });
48
+ // Firewall control-plane traffic is low-frequency and correctness-critical.
49
+ // Use non-keep-alive agents so clustered apps do not reuse sockets that an
50
+ // upstream load balancer has silently closed between sync cycles.
51
+ const _httpAgent = new http.Agent({ keepAlive: false });
52
+ const _httpsAgent = new https.Agent({ keepAlive: false });
51
53
  const EVENT_FLUSH_INTERVAL_MS = 2_000;
52
54
  const EVENT_BATCH_SIZE = 25;
53
55
  const EVENT_QUEUE_MAX = 1_000;
56
+ const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
54
57
 
55
58
  // Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
56
59
  let _useUnifiedSync = true;
@@ -119,67 +122,95 @@ function jitter(baseMs) {
119
122
  return baseMs * (0.8 + Math.random() * 0.4);
120
123
  }
121
124
 
122
- function agentFor(url) {
123
- return url.startsWith('https') ? _httpsAgent : _httpAgent;
124
- }
125
-
126
- function httpGet(url, extraHeaders, timeout, callback) {
127
- const mod = url.startsWith('https') ? https : http;
128
- const parsed = new URL(url);
129
-
130
- const req = mod.request({
131
- hostname: parsed.hostname,
132
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
133
- path: parsed.pathname + parsed.search,
134
- method: 'GET',
135
- headers: {
136
- 'Authorization': `Bearer ${_options.apiKey}`,
137
- 'User-Agent': 'securenow-firewall-sdk',
138
- ...extraHeaders,
139
- },
140
- timeout,
141
- agent: agentFor(url),
142
- }, (res) => {
143
- let data = '';
144
- res.on('data', (chunk) => { data += chunk; });
145
- res.on('end', () => { callback(null, res, data); });
146
- });
147
-
148
- req.on('error', (err) => callback(err));
149
- req.on('timeout', () => { req.destroy(); callback(new Error('Request timed out')); });
150
- req.end();
125
+ function agentFor(url) {
126
+ return url.startsWith('https') ? _httpsAgent : _httpAgent;
151
127
  }
152
128
 
153
- function httpPostJson(url, body, timeout, callback) {
129
+ function isTransientNetworkError(err) {
130
+ if (!err) return false;
131
+ if (err.code && TRANSIENT_NETWORK_CODES.has(err.code)) return true;
132
+ return /socket hang up|connection reset|ECONNRESET/i.test(String(err.message || ''));
133
+ }
134
+
135
+ function formatRequestError(err) {
136
+ if (!err) return 'unknown error';
137
+ const parts = [err.message || String(err)];
138
+ if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
139
+ if (err.syscall) parts.push(`syscall=${err.syscall}`);
140
+ return parts.join(' ');
141
+ }
142
+
143
+ function requestOnce(method, url, body, extraHeaders, timeout, callback) {
154
144
  const mod = url.startsWith('https') ? https : http;
155
145
  const parsed = new URL(url);
156
- const payload = JSON.stringify(body || {});
146
+ const payload = body == null ? null : JSON.stringify(body || {});
157
147
 
158
148
  const req = mod.request({
159
149
  hostname: parsed.hostname,
160
150
  port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
161
151
  path: parsed.pathname + parsed.search,
162
- method: 'POST',
152
+ method,
163
153
  headers: {
164
154
  'Authorization': `Bearer ${_options.apiKey}`,
165
155
  'User-Agent': 'securenow-firewall-sdk',
166
- 'Content-Type': 'application/json',
167
- 'Content-Length': Buffer.byteLength(payload),
156
+ 'Connection': 'close',
157
+ ...(payload
158
+ ? {
159
+ 'Content-Type': 'application/json',
160
+ 'Content-Length': Buffer.byteLength(payload),
161
+ }
162
+ : {}),
163
+ ...extraHeaders,
168
164
  },
169
165
  timeout,
170
166
  agent: agentFor(url),
171
167
  }, (res) => {
172
168
  let data = '';
173
169
  res.on('data', (chunk) => { data += chunk; });
174
- res.on('end', () => { callback(null, res, data); });
170
+ res.on('end', () => { done(null, res, data); });
175
171
  });
176
172
 
177
- req.on('error', (err) => callback(err));
178
- req.on('timeout', () => { req.destroy(); callback(new Error('Request timed out')); });
179
- req.write(payload);
173
+ let finished = false;
174
+ function done(...args) {
175
+ if (finished) return;
176
+ finished = true;
177
+ callback(...args);
178
+ }
179
+
180
+ req.on('error', (err) => done(err));
181
+ req.on('timeout', () => {
182
+ const err = new Error('Request timed out');
183
+ err.code = 'ETIMEDOUT';
184
+ done(err);
185
+ req.destroy(err);
186
+ });
187
+ if (payload) req.write(payload);
180
188
  req.end();
181
189
  }
182
190
 
191
+ function requestWithRetry(method, url, body, extraHeaders, timeout, callback) {
192
+ requestOnce(method, url, body, extraHeaders, timeout, (err, res, data) => {
193
+ if (!err || !isTransientNetworkError(err)) return callback(err, res, data);
194
+
195
+ if (_options && _options.log) {
196
+ console.warn('[securenow] Firewall: transient API socket error, retrying once:', formatRequestError(err));
197
+ }
198
+
199
+ const retryTimer = setTimeout(() => {
200
+ requestOnce(method, url, body, extraHeaders, timeout, callback);
201
+ }, 100);
202
+ if (retryTimer.unref) retryTimer.unref();
203
+ });
204
+ }
205
+
206
+ function httpGet(url, extraHeaders, timeout, callback) {
207
+ requestWithRetry('GET', url, null, extraHeaders, timeout, callback);
208
+ }
209
+
210
+ function httpPostJson(url, body, timeout, callback) {
211
+ requestWithRetry('POST', url, body, {}, timeout, callback);
212
+ }
213
+
183
214
  function scheduleEventFlush() {
184
215
  if (_eventTimer || _eventQueue.length === 0) return;
185
216
  _eventTimer = setTimeout(() => {
@@ -202,7 +233,7 @@ function flushFirewallEvents() {
202
233
  httpPostJson(url, { events: batch }, 5000, (err, res) => {
203
234
  if (err || !res || res.statusCode >= 400) {
204
235
  if (_options.log) {
205
- const msg = err ? err.message : `API returned ${res.statusCode}`;
236
+ const msg = err ? formatRequestError(err) : `API returned ${res.statusCode}`;
206
237
  console.warn('[securenow] Firewall: failed to report blocked-request ledger:', msg);
207
238
  }
208
239
  }
@@ -448,19 +479,19 @@ function doLegacyPoll(callback) {
448
479
  callback(null, { blChanged, alChanged });
449
480
  }
450
481
 
451
- if (blChanged) {
452
- legacyBlocklistSync((err, changed, stats) => {
453
- if (err && _options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err.message);
454
- else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
455
- syncDone();
456
- });
457
- }
458
- if (alChanged) {
459
- legacyAllowlistSync((err, changed, stats) => {
460
- if (err && _options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err.message);
461
- else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
462
- syncDone();
463
- });
482
+ if (blChanged) {
483
+ legacyBlocklistSync((err, changed, stats) => {
484
+ if (err && _options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', formatRequestError(err));
485
+ else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
486
+ syncDone();
487
+ });
488
+ }
489
+ if (alChanged) {
490
+ legacyAllowlistSync((err, changed, stats) => {
491
+ if (err && _options.log) console.warn('[securenow] Firewall: allowlist sync failed:', formatRequestError(err));
492
+ else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
493
+ syncDone();
494
+ });
464
495
  }
465
496
  }
466
497
 
@@ -495,7 +526,7 @@ function pollOnce(callback) {
495
526
  _consecutiveErrors++;
496
527
  _stats.errors++;
497
528
  maybeOpenCircuit();
498
- if (_options.log) console.warn('[securenow] Firewall: poll failed:', err.message);
529
+ if (_options.log) console.warn('[securenow] Firewall: poll failed:', formatRequestError(err));
499
530
  return callback(err);
500
531
  }
501
532
  _consecutiveErrors = 0;
@@ -565,7 +596,7 @@ function startSyncLoop() {
565
596
  if (retryTimer.unref) retryTimer.unref();
566
597
  return;
567
598
  }
568
- if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
599
+ if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', formatRequestError(err));
569
600
  if (_options.failMode === 'closed') {
570
601
  _matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
571
602
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.6.5",
3
+ "version": "7.6.7",
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",