securenow 7.7.14 → 7.7.16
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/NPM_README.md +65 -121
- package/README.md +19 -24
- package/SKILL-API.md +491 -490
- package/SKILL-CLI.md +8 -8
- package/app-config.js +146 -43
- package/cli/apps.js +589 -597
- package/cli/auth.js +1 -3
- package/cli/config.js +37 -9
- package/cli/credentials.js +1 -1
- package/cli/diagnostics.js +40 -10
- package/cli/init.js +1 -0
- package/firewall-only.js +1 -0
- package/firewall.js +62 -10
- package/free-trial-banner.js +2 -2
- package/mcp/catalog.js +2 -2
- package/nextjs.d.ts +67 -63
- package/nextjs.js +93 -52
- package/nuxt-server-plugin.mjs +7 -11
- package/nuxt.d.ts +42 -38
- package/nuxt.mjs +1 -1
- package/package.json +1 -1
- package/tracing.d.ts +2 -1
- package/tracing.js +75 -57
- package/web-vite.mjs +105 -15
package/cli/auth.js
CHANGED
|
@@ -75,7 +75,6 @@ async function loginWithBrowser() {
|
|
|
75
75
|
const returnedState = url.searchParams.get('state');
|
|
76
76
|
const appKey = url.searchParams.get('app_key');
|
|
77
77
|
const appName = url.searchParams.get('app_name');
|
|
78
|
-
const appInstance = url.searchParams.get('app_instance');
|
|
79
78
|
const apiKey = url.searchParams.get('api_key');
|
|
80
79
|
|
|
81
80
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -96,11 +95,10 @@ async function loginWithBrowser() {
|
|
|
96
95
|
|
|
97
96
|
if (token) {
|
|
98
97
|
pendingToken = token;
|
|
99
|
-
if (appKey || appName
|
|
98
|
+
if (appKey || appName) {
|
|
100
99
|
pendingApp = {
|
|
101
100
|
key: appKey || null,
|
|
102
101
|
name: appName || null,
|
|
103
|
-
instance: appInstance || null,
|
|
104
102
|
};
|
|
105
103
|
}
|
|
106
104
|
if (apiKey && apiKey.startsWith('snk_live_')) {
|
package/cli/config.js
CHANGED
|
@@ -153,11 +153,10 @@ function getToken() {
|
|
|
153
153
|
function setAuth(token, email, expiresAt, { local = false, app = null, enableFirewall = false } = {}) {
|
|
154
154
|
const targetFile = credentialsFileForLocal(local);
|
|
155
155
|
const payload = { ...loadJSON(targetFile), token, email, expiresAt };
|
|
156
|
-
if (app && (app.key || app.name
|
|
156
|
+
if (app && (app.key || app.name)) {
|
|
157
157
|
payload.app = {
|
|
158
158
|
key: app.key || null,
|
|
159
159
|
name: app.name || null,
|
|
160
|
-
instance: app.instance || null,
|
|
161
160
|
};
|
|
162
161
|
}
|
|
163
162
|
saveJSON(
|
|
@@ -201,23 +200,52 @@ function setApp(app, { local } = {}) {
|
|
|
201
200
|
app: {
|
|
202
201
|
key: app.key || null,
|
|
203
202
|
name: app.name || null,
|
|
204
|
-
instance: app.instance || null,
|
|
205
203
|
},
|
|
206
204
|
}) || {});
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
function ensureLocalGitignore() {
|
|
210
208
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
211
|
-
const
|
|
209
|
+
const legacyEntry = '.securenow/';
|
|
210
|
+
const entries = [
|
|
211
|
+
'.securenow/credentials.json',
|
|
212
|
+
'.securenow/credentials.*.json',
|
|
213
|
+
'!.securenow/credentials.example.json',
|
|
214
|
+
'!.securenow/credentials.*.example.json',
|
|
215
|
+
];
|
|
212
216
|
try {
|
|
217
|
+
let content = '';
|
|
213
218
|
if (fs.existsSync(gitignorePath)) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lines = content ? content.split(/\r?\n/) : [];
|
|
223
|
+
let replacedLegacyEntry = false;
|
|
224
|
+
const nextLines = [];
|
|
225
|
+
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
if (line.trim() === legacyEntry) {
|
|
228
|
+
if (!replacedLegacyEntry) {
|
|
229
|
+
nextLines.push(...entries);
|
|
230
|
+
replacedLegacyEntry = true;
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
217
233
|
}
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
nextLines.push(line);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const hasLine = (entry) => nextLines.some(line => line.trim() === entry);
|
|
238
|
+
const missing = entries.filter(entry => !hasLine(entry));
|
|
239
|
+
if (!replacedLegacyEntry && missing.length === 0) return;
|
|
240
|
+
|
|
241
|
+
let nextContent = nextLines.join('\n').replace(/\s*$/, '');
|
|
242
|
+
|
|
243
|
+
if (missing.length > 0) {
|
|
244
|
+
const block = ['# SecureNow local credential files', '# Keep .securenow/ itself trackable for repo-owned docs/templates.', ...missing].join('\n');
|
|
245
|
+
nextContent = nextContent ? `${nextContent}\n\n${block}` : block;
|
|
220
246
|
}
|
|
247
|
+
|
|
248
|
+
fs.writeFileSync(gitignorePath, `${nextContent}\n`);
|
|
221
249
|
} catch {}
|
|
222
250
|
}
|
|
223
251
|
|
package/cli/credentials.js
CHANGED
|
@@ -25,7 +25,6 @@ function buildRuntimeCredentials(options = {}) {
|
|
|
25
25
|
app: {
|
|
26
26
|
key: creds.app?.key || null,
|
|
27
27
|
name: creds.app?.name || null,
|
|
28
|
-
instance: creds.app?.instance || appConfig.FREE_TRIAL_INSTANCE,
|
|
29
28
|
},
|
|
30
29
|
config: {
|
|
31
30
|
...(creds.config || {}),
|
|
@@ -41,6 +40,7 @@ function buildRuntimeCredentials(options = {}) {
|
|
|
41
40
|
_securenow: {
|
|
42
41
|
...(creds._securenow || {}),
|
|
43
42
|
note: 'Runtime SecureNow credentials and SDK defaults. Mount or copy this JSON as .securenow/credentials.json or .securenow/credentials.<environment>.json in production. Do not commit it.',
|
|
43
|
+
routing: 'Telemetry is sent to the default SecureNow ingestion gateway. The gateway routes by app.key, so runtime credentials do not expose per-instance collector URLs.',
|
|
44
44
|
runtimeOnly: 'This file intentionally omits CLI OAuth fields: token, email, and expiresAt.',
|
|
45
45
|
production: 'Production can use this same file shape instead of environment variables.',
|
|
46
46
|
},
|
package/cli/diagnostics.js
CHANGED
|
@@ -36,10 +36,12 @@ function resolvedConfig(options = {}) {
|
|
|
36
36
|
apiKey,
|
|
37
37
|
firewallLocalEnabled,
|
|
38
38
|
apiUrl: config.getApiUrl(),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
firewallApiUrl: firewall.apiUrl,
|
|
40
|
+
firewallSyncEndpoint: `${firewall.apiUrl.replace(/\/$/, '')}/api/v1/firewall/sync`,
|
|
41
|
+
loggingEnabled: appConfig.boolConfig('logging.enabled', true),
|
|
42
|
+
captureBody: appConfig.boolConfig('capture.body', true),
|
|
43
|
+
captureMultipart: appConfig.boolConfig('capture.multipart', true),
|
|
44
|
+
otelLogLevel: appConfig.configValue('otel.logLevel', 'error') || 'error',
|
|
43
45
|
firewallEnabled,
|
|
44
46
|
firewallLayers: {
|
|
45
47
|
http: firewallEnabled,
|
|
@@ -330,11 +332,12 @@ async function logSend(args, flags) {
|
|
|
330
332
|
}
|
|
331
333
|
}
|
|
332
334
|
|
|
333
|
-
function okHttpStatus(status) {
|
|
335
|
+
function okHttpStatus(status, okStatuses) {
|
|
336
|
+
if (Array.isArray(okStatuses) && okStatuses.length) return okStatuses.includes(status);
|
|
334
337
|
return status >= 200 && status < 300;
|
|
335
338
|
}
|
|
336
339
|
|
|
337
|
-
async function probe({ endpoint, method = 'POST', headers = {}, body = null, timeoutMs = 3000 }) {
|
|
340
|
+
async function probe({ endpoint, method = 'POST', headers = {}, body = null, timeoutMs = 3000, okStatuses = null }) {
|
|
338
341
|
try {
|
|
339
342
|
const res = await httpRequest({
|
|
340
343
|
method,
|
|
@@ -344,9 +347,9 @@ async function probe({ endpoint, method = 'POST', headers = {}, body = null, tim
|
|
|
344
347
|
timeoutMs,
|
|
345
348
|
});
|
|
346
349
|
return {
|
|
347
|
-
ok: okHttpStatus(res.status),
|
|
350
|
+
ok: okHttpStatus(res.status, okStatuses),
|
|
348
351
|
status: res.status,
|
|
349
|
-
...(okHttpStatus(res.status) ? {} : { error: `HTTP ${res.status}`, body: res.body ? res.body.slice(0, 500) : '' }),
|
|
352
|
+
...(okHttpStatus(res.status, okStatuses) ? {} : { error: `HTTP ${res.status}`, body: res.body ? res.body.slice(0, 500) : '' }),
|
|
350
353
|
};
|
|
351
354
|
} catch (err) {
|
|
352
355
|
return { ok: false, error: err.message };
|
|
@@ -372,6 +375,8 @@ function env(_args, flags) {
|
|
|
372
375
|
otlpHeaders: Object.keys(cfg.headers || {}).length ? '***' : null,
|
|
373
376
|
firewallApiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
|
|
374
377
|
apiUrl: cfg.apiUrl,
|
|
378
|
+
firewallApiUrl: cfg.firewallApiUrl,
|
|
379
|
+
firewallSyncEndpoint: cfg.firewallSyncEndpoint,
|
|
375
380
|
loggingEnabled: cfg.loggingEnabled,
|
|
376
381
|
otelLogLevel: cfg.otelLogLevel,
|
|
377
382
|
captureBody: cfg.captureBody,
|
|
@@ -397,6 +402,7 @@ function env(_args, flags) {
|
|
|
397
402
|
['Body capture', cfg.captureBody ? ui.c.green('enabled') : ui.c.dim('disabled')],
|
|
398
403
|
['Multipart capture', cfg.captureMultipart ? ui.c.green('enabled') : ui.c.dim('disabled')],
|
|
399
404
|
['Firewall', firewallStatusLabel(cfg)],
|
|
405
|
+
['Firewall sync', cfg.firewallEnabled ? cfg.firewallSyncEndpoint : ui.c.dim('(not active)')],
|
|
400
406
|
]);
|
|
401
407
|
|
|
402
408
|
ui.heading('Resolved credentials');
|
|
@@ -454,6 +460,26 @@ async function doctor(_args, flags) {
|
|
|
454
460
|
checks.push({ name: 'api', ...api });
|
|
455
461
|
}
|
|
456
462
|
|
|
463
|
+
if (cfg.apiKey && cfg.firewallLocalEnabled) {
|
|
464
|
+
const query = new URLSearchParams();
|
|
465
|
+
if (cfg.appKey) query.set('app', cfg.appKey);
|
|
466
|
+
if (cfg.deploymentEnvironment) query.set('env', cfg.deploymentEnvironment);
|
|
467
|
+
const endpoint = `${cfg.firewallSyncEndpoint}${query.toString() ? `?${query.toString()}` : ''}`;
|
|
468
|
+
const spin4 = ui.spinner(`Probing firewall sync ${endpoint}`);
|
|
469
|
+
const sync = await probe({
|
|
470
|
+
method: 'GET',
|
|
471
|
+
endpoint,
|
|
472
|
+
headers: {
|
|
473
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
474
|
+
'X-SecureNow-Environment': cfg.deploymentEnvironment,
|
|
475
|
+
},
|
|
476
|
+
okStatuses: [200, 304],
|
|
477
|
+
});
|
|
478
|
+
if (sync.ok) spin4.stop(`Firewall sync reachable (HTTP ${sync.status})`);
|
|
479
|
+
else spin4.fail(`Firewall sync unreachable: ${sync.error}`);
|
|
480
|
+
checks.push({ name: 'firewall-sync', ...sync });
|
|
481
|
+
}
|
|
482
|
+
|
|
457
483
|
const warnings = [];
|
|
458
484
|
const singletonOkMessage = otelApiCheck.ok && otelApiCheck.packages.length
|
|
459
485
|
? `OpenTelemetry API singleton OK (${otelApiCheck.versions[0] || 'unknown'})`
|
|
@@ -467,8 +493,12 @@ async function doctor(_args, flags) {
|
|
|
467
493
|
if (!cfg.appKey) {
|
|
468
494
|
warnings.push('No app key resolved. Run `npx securenow login` or set app.key in .securenow/credentials.json.');
|
|
469
495
|
}
|
|
470
|
-
if (cfg.instance === 'https://
|
|
471
|
-
warnings.push('Using the
|
|
496
|
+
if (cfg.instance === 'https://ingest.securenow.ai') {
|
|
497
|
+
warnings.push('Using the SecureNow ingest gateway. Dedicated instances are routed server-side after policy checks.');
|
|
498
|
+
} else if (cfg.instance === 'https://api.securenow.ai/api/otlp') {
|
|
499
|
+
warnings.push('Using the legacy SecureNow telemetry gateway path. Regenerate credentials so telemetry uses https://ingest.securenow.ai.');
|
|
500
|
+
} else if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
|
|
501
|
+
warnings.push('Using the legacy free-trial collector directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
|
|
472
502
|
}
|
|
473
503
|
if (!cfg.apiKey && token) {
|
|
474
504
|
warnings.push('CLI/MCP is authenticated with your SecureNow session token. Runtime firewall enforcement key is missing. Run `npx securenow login` or `npx securenow api-key set snk_live_...` to refresh .securenow/credentials.json.');
|
package/cli/init.js
CHANGED
|
@@ -234,6 +234,7 @@ function printAgentPrompt(kind, filename, major, project) {
|
|
|
234
234
|
console.log([
|
|
235
235
|
'Set up SecureNow in this existing Next.js project without using .env files.',
|
|
236
236
|
'Use .securenow/credentials.json for local and production configuration; do not add .env files.',
|
|
237
|
+
'Ignore only .securenow/credentials.json and .securenow/credentials.*.json in git; keep the .securenow/ directory itself trackable for repo-owned files.',
|
|
237
238
|
commands
|
|
238
239
|
? `Use the project scripts for verification when appropriate: ${commands}. Ask before starting long-running dev/start servers, and ask which command to use if these scripts are not the right customer workflow.`
|
|
239
240
|
: 'Ask the customer which command starts, builds, and tests this app because package.json does not expose an obvious script.',
|
package/firewall-only.js
CHANGED
|
@@ -26,6 +26,7 @@ if (firewallOptions.apiKey && firewallOptions.enabled) {
|
|
|
26
26
|
appKey: firewallOptions.appKey,
|
|
27
27
|
environment: firewallOptions.environment,
|
|
28
28
|
apiUrl: firewallOptions.apiUrl,
|
|
29
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
29
30
|
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
30
31
|
syncInterval: firewallOptions.syncInterval,
|
|
31
32
|
failMode: firewallOptions.failMode,
|
package/firewall.js
CHANGED
|
@@ -21,6 +21,7 @@ let _stats = { syncs: 0, blocked: 0, rateLimited: 0, allowed: 0, versionChecks:
|
|
|
21
21
|
let _localhostFallbackTried = false;
|
|
22
22
|
let _eventQueue = [];
|
|
23
23
|
let _eventTimer = null;
|
|
24
|
+
let _remainingApiUrlFallbacks = [];
|
|
24
25
|
|
|
25
26
|
// Remote toggle - set by /firewall/sync when an appKey is in scope. Default
|
|
26
27
|
// true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
|
|
@@ -60,7 +61,7 @@ const _httpsAgent = new https.Agent({ keepAlive: false });
|
|
|
60
61
|
const EVENT_FLUSH_INTERVAL_MS = 2_000;
|
|
61
62
|
const EVENT_BATCH_SIZE = 25;
|
|
62
63
|
const EVENT_QUEUE_MAX = 1_000;
|
|
63
|
-
const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
|
|
64
|
+
const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
64
65
|
|
|
65
66
|
// Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
|
|
66
67
|
let _useUnifiedSync = true;
|
|
@@ -156,7 +157,13 @@ function agentFor(url) {
|
|
|
156
157
|
function isTransientNetworkError(err) {
|
|
157
158
|
if (!err) return false;
|
|
158
159
|
if (err.code && TRANSIENT_NETWORK_CODES.has(err.code)) return true;
|
|
159
|
-
return /socket hang up|connection reset|ECONNRESET/i.test(String(err.message || ''));
|
|
160
|
+
return /socket hang up|connection reset|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(String(err.message || ''));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isApiReachabilityError(err) {
|
|
164
|
+
if (isTransientNetworkError(err)) return true;
|
|
165
|
+
const text = `${err && err.code || ''} ${err && err.message || ''}`;
|
|
166
|
+
return /TLS|SSL|certificate|CERT_|UNABLE_TO_VERIFY|self signed/i.test(text);
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
function formatRequestError(err) {
|
|
@@ -167,6 +174,33 @@ function formatRequestError(err) {
|
|
|
167
174
|
return parts.join(' ');
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
function resetApiUrlFallbacks() {
|
|
178
|
+
const seen = new Set([_options && _options.apiUrl].filter(Boolean));
|
|
179
|
+
_remainingApiUrlFallbacks = [];
|
|
180
|
+
for (const candidate of Array.isArray(_options && _options.apiUrlFallbacks) ? _options.apiUrlFallbacks : []) {
|
|
181
|
+
const url = String(candidate || '').trim().replace(/\/$/, '');
|
|
182
|
+
if (!url || seen.has(url)) continue;
|
|
183
|
+
seen.add(url);
|
|
184
|
+
_remainingApiUrlFallbacks.push(url);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function switchToNextApiUrl(reason) {
|
|
189
|
+
if (!_options || _remainingApiUrlFallbacks.length === 0) return false;
|
|
190
|
+
const previous = _options.apiUrl;
|
|
191
|
+
_options.apiUrl = _remainingApiUrlFallbacks.shift();
|
|
192
|
+
_lastUnifiedEtag = null;
|
|
193
|
+
_lastSyncEtag = null;
|
|
194
|
+
_lastAllowlistSyncEtag = null;
|
|
195
|
+
if (_options.log) {
|
|
196
|
+
fwWarn('[securenow] Firewall: %s unreachable (%s), retrying sync via %s',
|
|
197
|
+
previous,
|
|
198
|
+
reason || 'network error',
|
|
199
|
+
_options.apiUrl);
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
function requestOnce(method, url, body, extraHeaders, timeout, callback) {
|
|
171
205
|
const mod = url.startsWith('https') ? https : http;
|
|
172
206
|
const parsed = new URL(url);
|
|
@@ -562,9 +596,15 @@ function pollOnce(callback) {
|
|
|
562
596
|
|
|
563
597
|
const done = (err, result) => {
|
|
564
598
|
_pollInflight = false;
|
|
565
|
-
if (err) {
|
|
566
|
-
|
|
567
|
-
|
|
599
|
+
if (err) {
|
|
600
|
+
if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
|
|
601
|
+
_pollInflight = false;
|
|
602
|
+
const retryTimer = setTimeout(() => pollOnce(callback), 1000);
|
|
603
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
_consecutiveErrors++;
|
|
607
|
+
_stats.errors++;
|
|
568
608
|
maybeOpenCircuit();
|
|
569
609
|
if (_options.log) fwWarn('[securenow] Firewall: poll failed:', formatRequestError(err));
|
|
570
610
|
return callback(err);
|
|
@@ -623,9 +663,14 @@ function startSyncLoop() {
|
|
|
623
663
|
const syncFn = _useUnifiedSync ? doUnifiedSync : (cb) => doLegacyPoll(cb);
|
|
624
664
|
|
|
625
665
|
syncFn((err, result) => {
|
|
626
|
-
if (err) {
|
|
627
|
-
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
628
|
-
if (
|
|
666
|
+
if (err) {
|
|
667
|
+
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
668
|
+
if (isApiReachabilityError(err) && switchToNextApiUrl(formatRequestError(err))) {
|
|
669
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
670
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
|
|
629
674
|
_localhostFallbackTried = true;
|
|
630
675
|
const origUrl = _options.apiUrl;
|
|
631
676
|
_options.apiUrl = 'http://localhost:4000';
|
|
@@ -981,10 +1026,15 @@ function patchHttpLayer() {
|
|
|
981
1026
|
|
|
982
1027
|
// Init
|
|
983
1028
|
|
|
984
|
-
function init(options) {
|
|
985
|
-
_options = options;
|
|
1029
|
+
function init(options) {
|
|
1030
|
+
_options = options || {};
|
|
1031
|
+
_options.apiUrl = String(_options.apiUrl || '').trim().replace(/\/$/, '');
|
|
1032
|
+
_localhostFallbackTried = false;
|
|
1033
|
+
_useUnifiedSync = true;
|
|
1034
|
+
resetApiUrlFallbacks();
|
|
986
1035
|
|
|
987
1036
|
if (_options.log) fwLog('[securenow] Firewall: ENABLED');
|
|
1037
|
+
if (_options.log && _options.apiUrl) fwLog('[securenow] Firewall: sync endpoint=%s/api/v1/firewall/sync', _options.apiUrl);
|
|
988
1038
|
if (_options.log && _options.environment) fwLog('[securenow] Firewall: environment=%s', _options.environment);
|
|
989
1039
|
|
|
990
1040
|
patchHttpLayer();
|
|
@@ -1043,8 +1093,10 @@ function shutdown() {
|
|
|
1043
1093
|
_consecutiveErrors = 0;
|
|
1044
1094
|
_pollInflight = false;
|
|
1045
1095
|
_retryAfterUntil = 0;
|
|
1096
|
+
_localhostFallbackTried = false;
|
|
1046
1097
|
_rateLimitRules = [];
|
|
1047
1098
|
_rateLimitBuckets = new Map();
|
|
1099
|
+
_remainingApiUrlFallbacks = [];
|
|
1048
1100
|
|
|
1049
1101
|
_httpAgent.destroy();
|
|
1050
1102
|
_httpsAgent.destroy();
|
package/free-trial-banner.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* Opt-out: set config.runtime.hideBanner=true in .securenow/credentials.json
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const FREE_TRIAL_HOSTS = ['ingest.securenow.ai', 'freetrial.securenow.ai'];
|
|
11
11
|
|
|
12
12
|
function isFreeTrial(endpointBase) {
|
|
13
|
-
return !!endpointBase && endpointBase.includes(
|
|
13
|
+
return !!endpointBase && FREE_TRIAL_HOSTS.some((host) => endpointBase.includes(host));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/* istanbul ignore next — runs in browser, not Node */
|
package/mcp/catalog.js
CHANGED
|
@@ -17,7 +17,7 @@ Primary goals:
|
|
|
17
17
|
|
|
18
18
|
Safety rules:
|
|
19
19
|
- Do not print full API keys, JWTs, tokens, or .securenow/credentials.json. Mask secrets.
|
|
20
|
-
- Do not commit secrets.
|
|
20
|
+
- Do not commit secrets. Ignore only local SecureNow credential files (.securenow/credentials.json and .securenow/credentials.*.json); keep the .securenow/ directory itself trackable for repo-owned docs/templates.
|
|
21
21
|
- Do not manually browse to a SecureNow auth URL. Always start auth with npx securenow login so the CLI generates the required callback and state.
|
|
22
22
|
- If the browser says "Missing callback parameter", you opened the wrong URL: rerun npx securenow login from the project root.
|
|
23
23
|
- Do not skip login, app selection, firewall connection, or verification unless I explicitly say to.
|
|
@@ -42,7 +42,7 @@ Runbook:
|
|
|
42
42
|
- Confirm .securenow/credentials.json exists.
|
|
43
43
|
- Confirm it has SecureNow's default config/explanations block.
|
|
44
44
|
- Confirm it has an app key/name/instance and a firewall API key after login/app selection.
|
|
45
|
-
- Confirm .securenow/
|
|
45
|
+
- Confirm .securenow/credentials.json and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
|
|
46
46
|
6. Run npx securenow init. If it fails with ui.header is not a function or another CLI bug, upgrade to securenow@latest, verify >=7.5.1, and retry. Do not silently ignore init failures.
|
|
47
47
|
7. Configure the least invasive framework-specific integration:
|
|
48
48
|
- Next.js: preserve instrumentation.js/ts. Register securenow/nextjs only when NEXT_RUNTIME is nodejs. In ESM files, use createRequire before require("securenow/nextjs"). Include require("securenow/nextjs-auto-capture") for body capture. For Next 15+, add securenow to serverExternalPackages. For older Next.js, use experimental.serverComponentsExternalPackages. Preserve proxy.js/middleware.js.
|
package/nextjs.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecureNow Next.js Integration TypeScript Declarations
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface RegisterOptions {
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Next.js Integration TypeScript Declarations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RegisterOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Service name for OpenTelemetry traces
|
|
8
8
|
* @default .securenow/credentials.json app.key/app.name
|
|
@@ -11,7 +11,11 @@ export interface RegisterOptions {
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* OTLP endpoint for traces
|
|
14
|
-
* @default
|
|
14
|
+
* @default https://ingest.securenow.ai
|
|
15
|
+
*
|
|
16
|
+
* Advanced OTLP endpoint override. Normal SecureNow apps should leave this
|
|
17
|
+
* unset so the ingest gateway can route by app.key to the dashboard-selected
|
|
18
|
+
* instance.
|
|
15
19
|
*/
|
|
16
20
|
endpoint?: string;
|
|
17
21
|
|
|
@@ -20,26 +24,26 @@ export interface RegisterOptions {
|
|
|
20
24
|
* @default .securenow/credentials.json config.runtime.deploymentEnvironment
|
|
21
25
|
*/
|
|
22
26
|
environment?: string;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Don't append UUID to service name
|
|
26
|
-
* @default false
|
|
27
|
-
*/
|
|
28
|
-
noUuid?: boolean;
|
|
29
|
-
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Don't append UUID to service name
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
noUuid?: boolean;
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* Enable request body capture
|
|
32
36
|
* @default true from .securenow/credentials.json secure defaults
|
|
33
37
|
*/
|
|
34
38
|
captureBody?: boolean;
|
|
35
39
|
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Register SecureNow OpenTelemetry instrumentation for Next.js
|
|
39
|
-
*
|
|
40
|
-
* @param options - Optional configuration options
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register SecureNow OpenTelemetry instrumentation for Next.js
|
|
43
|
+
*
|
|
44
|
+
* @param options - Optional configuration options
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
43
47
|
* ```typescript
|
|
44
48
|
* // instrumentation.ts
|
|
45
49
|
* import { createRequire } from 'node:module';
|
|
@@ -53,46 +57,46 @@ export interface RegisterOptions {
|
|
|
53
57
|
* require('securenow/nextjs-auto-capture');
|
|
54
58
|
* }
|
|
55
59
|
* ```
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* // With custom options
|
|
60
|
-
* import { registerSecureNow } from 'securenow/nextjs';
|
|
61
|
-
*
|
|
62
|
-
* export function register() {
|
|
63
|
-
* registerSecureNow({
|
|
64
|
-
* serviceName: 'my-nextjs-app',
|
|
65
|
-
* endpoint: 'http://your-otlp-backend.example.com:4318',
|
|
66
|
-
* noUuid: true,
|
|
67
|
-
* });
|
|
68
|
-
* }
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export function registerSecureNow(options?: RegisterOptions): void;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Default sensitive fields that are automatically redacted from traces
|
|
75
|
-
*/
|
|
76
|
-
export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Redact sensitive fields from an object
|
|
80
|
-
* @param obj - Object to redact
|
|
81
|
-
* @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
|
|
82
|
-
* @returns Redacted copy of the object
|
|
83
|
-
*/
|
|
84
|
-
export function redactSensitiveData<T = any>(
|
|
85
|
-
obj: T,
|
|
86
|
-
sensitiveFields?: string[]
|
|
87
|
-
): T;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Redact sensitive data from GraphQL query strings
|
|
91
|
-
* @param query - GraphQL query string
|
|
92
|
-
* @param sensitiveFields - Array of field names to redact
|
|
93
|
-
* @returns Redacted query string
|
|
94
|
-
*/
|
|
95
|
-
export function redactGraphQLQuery(
|
|
96
|
-
query: string,
|
|
97
|
-
sensitiveFields?: string[]
|
|
98
|
-
): string;
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // With custom options
|
|
64
|
+
* import { registerSecureNow } from 'securenow/nextjs';
|
|
65
|
+
*
|
|
66
|
+
* export function register() {
|
|
67
|
+
* registerSecureNow({
|
|
68
|
+
* serviceName: 'my-nextjs-app',
|
|
69
|
+
* endpoint: 'http://your-otlp-backend.example.com:4318',
|
|
70
|
+
* noUuid: true,
|
|
71
|
+
* });
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function registerSecureNow(options?: RegisterOptions): void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default sensitive fields that are automatically redacted from traces
|
|
79
|
+
*/
|
|
80
|
+
export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Redact sensitive fields from an object
|
|
84
|
+
* @param obj - Object to redact
|
|
85
|
+
* @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
|
|
86
|
+
* @returns Redacted copy of the object
|
|
87
|
+
*/
|
|
88
|
+
export function redactSensitiveData<T = any>(
|
|
89
|
+
obj: T,
|
|
90
|
+
sensitiveFields?: string[]
|
|
91
|
+
): T;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Redact sensitive data from GraphQL query strings
|
|
95
|
+
* @param query - GraphQL query string
|
|
96
|
+
* @param sensitiveFields - Array of field names to redact
|
|
97
|
+
* @returns Redacted query string
|
|
98
|
+
*/
|
|
99
|
+
export function redactGraphQLQuery(
|
|
100
|
+
query: string,
|
|
101
|
+
sensitiveFields?: string[]
|
|
102
|
+
): string;
|