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 +2 -2
- package/cli/config.js +20 -5
- package/cli/credentials.js +5 -1
- package/cli/init.js +1 -1
- package/firewall.js +88 -57
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
package/cli/credentials.js
CHANGED
|
@@ -20,7 +20,7 @@ function buildRuntimeCredentials(options = {}) {
|
|
|
20
20
|
options.env ||
|
|
21
21
|
appConfig.resolveDeploymentEnvironment() ||
|
|
22
22
|
'production';
|
|
23
|
-
const runtime =
|
|
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
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
152
|
+
method,
|
|
163
153
|
headers: {
|
|
164
154
|
'Authorization': `Bearer ${_options.apiKey}`,
|
|
165
155
|
'User-Agent': 'securenow-firewall-sdk',
|
|
166
|
-
'
|
|
167
|
-
|
|
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', () => {
|
|
170
|
+
res.on('end', () => { done(null, res, data); });
|
|
175
171
|
});
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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