securenow 8.12.4 → 8.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +2 -0
- package/README.md +21 -5
- package/SKILL-API.md +3 -1
- package/SKILL-CLI.md +2 -0
- package/app-config.js +4 -0
- package/cli/firewall.js +47 -1
- package/cli/monitor.js +6 -5
- package/cli/rateLimits.js +3 -2
- package/cli.js +2 -1
- package/crash-capture.js +125 -0
- package/firewall.js +121 -10
- package/mcp/catalog.js +434 -25
- package/mcp/server.js +21 -13
- package/nextjs.js +9 -13
- package/package.json +4 -2
- package/rate-limit-store.js +175 -0
- package/tracing.js +19 -25
package/mcp/server.js
CHANGED
|
@@ -14,6 +14,9 @@ const {
|
|
|
14
14
|
promptMessages,
|
|
15
15
|
assertConfirmed,
|
|
16
16
|
buildApiRequest,
|
|
17
|
+
pollToolResult,
|
|
18
|
+
fitToolResult,
|
|
19
|
+
summarizeToolResult,
|
|
17
20
|
resourceContent,
|
|
18
21
|
jsonText,
|
|
19
22
|
maskSecret,
|
|
@@ -160,27 +163,32 @@ async function callApiTool(tool, args) {
|
|
|
160
163
|
throw new Error('Admin auth is missing. Run `npx securenow admin login` from the project root, then retry. Runtime app credentials cannot grant admin/control-plane permissions.');
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
const send = (request) => {
|
|
167
|
+
const options = {
|
|
168
|
+
token,
|
|
169
|
+
...(Object.keys(request.query || {}).length > 0 ? { query: request.query } : {}),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (request.method === 'GET') return api.get(request.endpoint, options);
|
|
173
|
+
if (request.method === 'POST') return api.post(request.endpoint, request.body || {}, options);
|
|
174
|
+
if (request.method === 'PUT') return api.put(request.endpoint, request.body || {}, options);
|
|
175
|
+
if (request.method === 'PATCH') return api.patch(request.endpoint, request.body || {}, options);
|
|
176
|
+
if (request.method === 'DELETE') return api.delete(request.endpoint, options);
|
|
168
177
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (request.method === 'PUT') return api.put(request.endpoint, request.body || {}, options);
|
|
172
|
-
if (request.method === 'PATCH') return api.patch(request.endpoint, request.body || {}, options);
|
|
173
|
-
if (request.method === 'DELETE') return api.delete(request.endpoint, options);
|
|
178
|
+
throw new Error(`Unsupported method for ${tool.name}: ${request.method}`);
|
|
179
|
+
};
|
|
174
180
|
|
|
175
|
-
|
|
181
|
+
const result = await send(buildApiRequest(tool, finalArgs));
|
|
182
|
+
return summarizeToolResult(tool, await pollToolResult(tool, result, send));
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
function asToolResult(
|
|
185
|
+
function asToolResult(rawResult) {
|
|
186
|
+
const result = fitToolResult(rawResult);
|
|
179
187
|
return {
|
|
180
188
|
content: [
|
|
181
189
|
{
|
|
182
190
|
type: 'text',
|
|
183
|
-
text: typeof result === 'string' ? result :
|
|
191
|
+
text: typeof result === 'string' ? result : JSON.stringify(result),
|
|
184
192
|
},
|
|
185
193
|
],
|
|
186
194
|
structuredContent: typeof result === 'object' && result !== null ? result : { value: result },
|
package/nextjs.js
CHANGED
|
@@ -514,19 +514,14 @@ function registerSecureNow(options = {}) {
|
|
|
514
514
|
_formatOtlpError(err)
|
|
515
515
|
);
|
|
516
516
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
526
|
-
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
throw reason;
|
|
517
|
+
// Crash capture (see crash-capture.js). The logger provider only exists on
|
|
518
|
+
// self-hosted deployments; on Vercel crashes keep Node's default behaviour.
|
|
519
|
+
let _crashLoggerProvider = null;
|
|
520
|
+
require('./crash-capture').installCrashCapture({
|
|
521
|
+
getLogger: () => (_crashLoggerProvider ? _crashLoggerProvider.getLogger('securenow.crash', '1.0.0') : null),
|
|
522
|
+
flush: () => (_crashLoggerProvider ? _crashLoggerProvider.forceFlush() : undefined),
|
|
523
|
+
isSdkError: (e) => _isOtlpTransientError(e) && (_looksLikeOtlpStack(e) || _looksLikeConfiguredOtlpEndpoint(e)),
|
|
524
|
+
onSdkError: _reportSuppressedOtlpError,
|
|
530
525
|
});
|
|
531
526
|
|
|
532
527
|
if (isVercel) {
|
|
@@ -606,6 +601,7 @@ function registerSecureNow(options = {}) {
|
|
|
606
601
|
// sdk-logs >= 0.220 takes a single options object ({ exporter }).
|
|
607
602
|
processors: [new BatchLogRecordProcessor({ exporter: logExporter })],
|
|
608
603
|
});
|
|
604
|
+
_crashLoggerProvider = loggerProvider;
|
|
609
605
|
|
|
610
606
|
// Patch console to forward logs as OTLP log records
|
|
611
607
|
const logger = loggerProvider.getLogger('console', '1.0.0');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.0",
|
|
4
4
|
"mcpName": "ai.securenow/securenow",
|
|
5
5
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"securenow-mcp": "mcp/server.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node test-app-config.js && node test-resolve-ip.js && node test-otel-defaults.js && node test-telemetry.js && node test-legacy-semconv.js && node test-otp-redaction.js && node test-header-redaction.js && node test-firewall-paths.js && node test-log-format.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-package-contents.js"
|
|
14
|
+
"test": "node test-app-config.js && node test-resolve-ip.js && node test-otel-defaults.js && node test-telemetry.js && node test-legacy-semconv.js && node test-otp-redaction.js && node test-header-redaction.js && node test-firewall-paths.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-crash-capture.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-package-contents.js"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"opentelemetry",
|
|
@@ -164,7 +164,9 @@
|
|
|
164
164
|
"otp-redaction.js",
|
|
165
165
|
"header-redaction.js",
|
|
166
166
|
"firewall-paths.js",
|
|
167
|
+
"rate-limit-store.js",
|
|
167
168
|
"log-format.js",
|
|
169
|
+
"crash-capture.js",
|
|
168
170
|
"sdk-disabled.js",
|
|
169
171
|
"rate-limits.js",
|
|
170
172
|
"rate-limits.d.ts",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Optional shared counters for rate limits, so every worker and every server counts against
|
|
4
|
+
// one bucket instead of each process counting on its own.
|
|
5
|
+
//
|
|
6
|
+
// .securenow/runtime.json → "config": { "firewall": { "rateLimitStore": "redis://host:6379" } }
|
|
7
|
+
// (rediss:// for TLS; user:password@ and /db are supported)
|
|
8
|
+
//
|
|
9
|
+
// The firewall never waits on Redis: it decides from what it already knows (its own count plus
|
|
10
|
+
// the latest shared count Redis returned) and reports each counted request in the background.
|
|
11
|
+
// At most a request or two per process can slip past the limit while a reply is in flight.
|
|
12
|
+
// If Redis is unreachable the firewall keeps counting in-process (fail-open) and reconnects.
|
|
13
|
+
//
|
|
14
|
+
// No dependency: a minimal RESP client that only sends AUTH / SELECT / INCR / PEXPIRE.
|
|
15
|
+
|
|
16
|
+
const net = require('net');
|
|
17
|
+
const tls = require('tls');
|
|
18
|
+
|
|
19
|
+
const COMMAND_TIMEOUT_MS = 1000;
|
|
20
|
+
const MAX_BACKOFF_MS = 30000;
|
|
21
|
+
|
|
22
|
+
function encodeCommand(args) {
|
|
23
|
+
let out = `*${args.length}\r\n`;
|
|
24
|
+
for (const arg of args) {
|
|
25
|
+
const s = String(arg);
|
|
26
|
+
out += `$${Buffer.byteLength(s)}\r\n${s}\r\n`;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parses as many complete RESP replies as the buffer holds. Returns { replies, rest }.
|
|
32
|
+
function parseReplies(buffer) {
|
|
33
|
+
const replies = [];
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < buffer.length) {
|
|
36
|
+
const end = buffer.indexOf('\r\n', offset);
|
|
37
|
+
if (end === -1) break;
|
|
38
|
+
const type = buffer[offset];
|
|
39
|
+
const line = buffer.toString('utf8', offset + 1, end);
|
|
40
|
+
if (type === 0x2b /* + */) { replies.push({ ok: true, value: line }); offset = end + 2; continue; }
|
|
41
|
+
if (type === 0x2d /* - */) { replies.push({ ok: false, error: line }); offset = end + 2; continue; }
|
|
42
|
+
if (type === 0x3a /* : */) { replies.push({ ok: true, value: Number(line) }); offset = end + 2; continue; }
|
|
43
|
+
if (type === 0x24 /* $ */) {
|
|
44
|
+
const len = Number(line);
|
|
45
|
+
if (len === -1) { replies.push({ ok: true, value: null }); offset = end + 2; continue; }
|
|
46
|
+
if (buffer.length < end + 2 + len + 2) break;
|
|
47
|
+
replies.push({ ok: true, value: buffer.toString('utf8', end + 2, end + 2 + len) });
|
|
48
|
+
offset = end + 2 + len + 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Arrays and other types are never requested; treat as a protocol error.
|
|
52
|
+
replies.push({ ok: false, error: `unexpected reply type ${String.fromCharCode(type)}` });
|
|
53
|
+
offset = end + 2;
|
|
54
|
+
}
|
|
55
|
+
return { replies, rest: buffer.subarray(offset) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class RedisCounterStore {
|
|
59
|
+
constructor(url, { log } = {}) {
|
|
60
|
+
const parsed = new URL(url);
|
|
61
|
+
if (!['redis:', 'rediss:'].includes(parsed.protocol)) throw new Error('rateLimitStore must be a redis:// or rediss:// URL');
|
|
62
|
+
this.host = parsed.hostname || '127.0.0.1';
|
|
63
|
+
this.port = Number(parsed.port || 6379);
|
|
64
|
+
this.tls = parsed.protocol === 'rediss:';
|
|
65
|
+
this.username = decodeURIComponent(parsed.username || '');
|
|
66
|
+
this.password = decodeURIComponent(parsed.password || '');
|
|
67
|
+
this.db = Number((parsed.pathname || '/').slice(1) || 0) || 0;
|
|
68
|
+
this.log = typeof log === 'function' ? log : () => {};
|
|
69
|
+
this.socket = null;
|
|
70
|
+
this.ready = false;
|
|
71
|
+
this.pending = [];
|
|
72
|
+
this.buffer = Buffer.alloc(0);
|
|
73
|
+
this.backoff = 1000;
|
|
74
|
+
this.closed = false;
|
|
75
|
+
this.connect();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe() {
|
|
79
|
+
return `${this.tls ? 'rediss' : 'redis'}://${this.host}:${this.port}/${this.db}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
connect() {
|
|
83
|
+
if (this.closed) return;
|
|
84
|
+
const onConnect = () => {
|
|
85
|
+
const setup = [];
|
|
86
|
+
if (this.password) setup.push(this.username ? ['AUTH', this.username, this.password] : ['AUTH', this.password]);
|
|
87
|
+
if (this.db) setup.push(['SELECT', this.db]);
|
|
88
|
+
Promise.all(setup.map((cmd) => this.send(cmd, true)))
|
|
89
|
+
.then(() => {
|
|
90
|
+
this.ready = true;
|
|
91
|
+
this.backoff = 1000;
|
|
92
|
+
this.log('[securenow] Firewall: shared rate-limit counters ON (%s)', this.describe());
|
|
93
|
+
})
|
|
94
|
+
.catch((err) => {
|
|
95
|
+
this.log('[securenow] Firewall: rate-limit store setup failed (%s); counting in-process', err.message);
|
|
96
|
+
this.socket.destroy();
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const socket = this.tls
|
|
100
|
+
? tls.connect({ host: this.host, port: this.port, servername: this.host }, onConnect)
|
|
101
|
+
: net.connect({ host: this.host, port: this.port }, onConnect);
|
|
102
|
+
socket.setNoDelay(true);
|
|
103
|
+
socket.unref();
|
|
104
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
105
|
+
socket.on('error', () => { /* handled by close */ });
|
|
106
|
+
socket.on('close', () => this.onClose());
|
|
107
|
+
this.socket = socket;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onData(chunk) {
|
|
111
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
112
|
+
const { replies, rest } = parseReplies(this.buffer);
|
|
113
|
+
this.buffer = rest;
|
|
114
|
+
for (const reply of replies) {
|
|
115
|
+
const waiter = this.pending.shift();
|
|
116
|
+
if (!waiter) continue;
|
|
117
|
+
clearTimeout(waiter.timer);
|
|
118
|
+
if (reply.ok) waiter.resolve(reply.value);
|
|
119
|
+
else waiter.reject(new Error(reply.error));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onClose() {
|
|
124
|
+
const wasReady = this.ready;
|
|
125
|
+
this.ready = false;
|
|
126
|
+
this.buffer = Buffer.alloc(0);
|
|
127
|
+
const pending = this.pending;
|
|
128
|
+
this.pending = [];
|
|
129
|
+
for (const waiter of pending) { clearTimeout(waiter.timer); waiter.reject(new Error('connection closed')); }
|
|
130
|
+
if (this.closed) return;
|
|
131
|
+
if (wasReady) this.log('[securenow] Firewall: rate-limit store unreachable; counting in-process until it is back');
|
|
132
|
+
const delay = this.backoff;
|
|
133
|
+
this.backoff = Math.min(MAX_BACKOFF_MS, this.backoff * 2);
|
|
134
|
+
const timer = setTimeout(() => this.connect(), delay);
|
|
135
|
+
if (timer.unref) timer.unref();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
send(args, allowBeforeReady = false) {
|
|
139
|
+
if (!this.socket || (!this.ready && !allowBeforeReady)) return Promise.reject(new Error('store not ready'));
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const waiter = { resolve, reject, timer: null };
|
|
142
|
+
waiter.timer = setTimeout(() => {
|
|
143
|
+
reject(new Error('store timeout'));
|
|
144
|
+
// A lost reply desynchronizes the pipeline: drop the connection and start clean.
|
|
145
|
+
if (this.socket) this.socket.destroy();
|
|
146
|
+
}, COMMAND_TIMEOUT_MS);
|
|
147
|
+
if (waiter.timer.unref) waiter.timer.unref();
|
|
148
|
+
this.pending.push(waiter);
|
|
149
|
+
this.socket.write(encodeCommand(args));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Count one request in a fixed window key; resolves with the shared total for that window.
|
|
154
|
+
incr(key, ttlMs) {
|
|
155
|
+
if (!this.ready) return Promise.reject(new Error('store not ready'));
|
|
156
|
+
const counted = this.send(['INCR', key]);
|
|
157
|
+
this.send(['PEXPIRE', key, Math.max(1000, Math.round(ttlMs))]).catch(() => {});
|
|
158
|
+
return counted;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
close() {
|
|
162
|
+
this.closed = true;
|
|
163
|
+
this.ready = false;
|
|
164
|
+
if (this.socket) this.socket.destroy();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function createRateLimitStore(spec, options = {}) {
|
|
169
|
+
if (!spec) return null;
|
|
170
|
+
const url = typeof spec === 'string' ? spec : spec.url;
|
|
171
|
+
if (!url) return null;
|
|
172
|
+
return new RedisCounterStore(url, options);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { createRateLimitStore, RedisCounterStore, encodeCommand, parseReplies };
|
package/tracing.js
CHANGED
|
@@ -9,7 +9,7 @@ if (require('./sdk-disabled').sdkDisabled('tracing')) return;
|
|
|
9
9
|
* Preload with: node --require securenow/register app.js
|
|
10
10
|
*
|
|
11
11
|
* Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
|
|
12
|
-
* auto-registered via module.register()
|
|
12
|
+
* auto-registered via module.register() — no --import flag needed.
|
|
13
13
|
* On Node 18 with "type": "module", add the hook manually:
|
|
14
14
|
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
15
15
|
*
|
|
@@ -348,7 +348,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
348
348
|
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
349
349
|
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
350
350
|
if (!hasCliHook && !hasModuleRegister) {
|
|
351
|
-
console.warn('[securenow]
|
|
351
|
+
console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
352
352
|
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
353
353
|
}
|
|
354
354
|
}
|
|
@@ -384,7 +384,7 @@ const headers = resolvedEndpoints.headers;
|
|
|
384
384
|
const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
385
385
|
const baseName = rawBase || null;
|
|
386
386
|
// Auto-disables the per-worker suffix when we resolved a routing UUID from
|
|
387
|
-
// credentials
|
|
387
|
+
// credentials — the dashboard does exact-match IN on service.name, so any
|
|
388
388
|
// suffix breaks routing. config.runtime.noUuid can override.
|
|
389
389
|
const noUuid = appConfig.resolveNoUuid();
|
|
390
390
|
const strict = appConfig.boolConfig('runtime.strict', false);
|
|
@@ -411,7 +411,7 @@ const instancePrefix = baseName || 'securenow';
|
|
|
411
411
|
const serviceInstanceId = `${instancePrefix}-${randomUUID()}`;
|
|
412
412
|
|
|
413
413
|
// Loud line per worker to prove what was used
|
|
414
|
-
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s
|
|
414
|
+
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=%s instance.id=%s',
|
|
415
415
|
process.pid,
|
|
416
416
|
JSON.stringify(baseName),
|
|
417
417
|
JSON.stringify(endpointBase),
|
|
@@ -739,21 +739,15 @@ function _reportSuppressedOtlpError(kind, err, origin) {
|
|
|
739
739
|
);
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
752
|
-
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
753
|
-
return; // swallow
|
|
754
|
-
}
|
|
755
|
-
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
756
|
-
throw reason;
|
|
742
|
+
// Crashes: OTLP exporter blips are swallowed (never crash the host app); every
|
|
743
|
+
// other uncaught exception / unhandled rejection is recorded as a FATAL log
|
|
744
|
+
// record, flushed, and the process exits like Node's default (code 1). If the
|
|
745
|
+
// app has its own handler, the error is recorded and that handler decides.
|
|
746
|
+
require('./crash-capture').installCrashCapture({
|
|
747
|
+
getLogger: () => (loggerProvider ? loggerProvider.getLogger('securenow.crash', '1.0.0') : null),
|
|
748
|
+
flush: () => (loggerProvider ? loggerProvider.forceFlush() : undefined),
|
|
749
|
+
isSdkError: (e) => _isOtlpTransientError(e) && (_looksLikeOtlpStack(e) || _looksLikeConfiguredOtlpEndpoint(e)),
|
|
750
|
+
onSdkError: _reportSuppressedOtlpError,
|
|
757
751
|
});
|
|
758
752
|
|
|
759
753
|
// -------- SDK --------
|
|
@@ -779,17 +773,17 @@ const sdk = new NodeSDK({
|
|
|
779
773
|
(async () => {
|
|
780
774
|
try {
|
|
781
775
|
await Promise.resolve(sdk.start?.());
|
|
782
|
-
console.log('[securenow] OTel SDK started
|
|
776
|
+
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
783
777
|
if (loggingEnabled) {
|
|
784
|
-
console.log('[securenow]
|
|
778
|
+
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
785
779
|
} else {
|
|
786
780
|
console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
|
|
787
781
|
}
|
|
788
782
|
if (captureBody) {
|
|
789
|
-
console.log('[securenow]
|
|
783
|
+
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
790
784
|
}
|
|
791
785
|
if (captureMultipart) {
|
|
792
|
-
console.log('[securenow]
|
|
786
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
793
787
|
}
|
|
794
788
|
if (appConfig.boolConfig('runtime.testSpan', false)) {
|
|
795
789
|
const api = require('@opentelemetry/api');
|
|
@@ -797,9 +791,9 @@ const sdk = new NodeSDK({
|
|
|
797
791
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
798
792
|
}
|
|
799
793
|
|
|
800
|
-
// Firewall
|
|
794
|
+
// Firewall — auto-activates only when a real snk_live_ key is resolvable.
|
|
801
795
|
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
802
|
-
// only an app-routing UUID (or nothing at all)
|
|
796
|
+
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
803
797
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
804
798
|
if (firewallOptions.apiKey) {
|
|
805
799
|
require('./firewall').init({
|