securenow 8.24.0 → 8.25.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.
@@ -1,104 +1,162 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Layer 3: OS-level firewall via iptables/nftables.
5
- * Manages a dedicated SECURENOW_BLOCK chain — never touches customer rules.
4
+ * Layer 3: OS-level firewall via iptables (IPv4), ip6tables (IPv6) or, where neither exists, nftables.
5
+ * Manages its own chain (SECURENOW_BLOCK), or with nft its own table: never touches customer rules.
6
6
  * Linux only, requires root or CAP_NET_ADMIN. Falls back gracefully otherwise.
7
+ *
8
+ * Blocklist entries arrive over the network, so each one must be a literal IP or CIDR before it reaches
9
+ * a firewall command, and the binaries run without a shell. (Entries used to be pasted into a shell
10
+ * command run as root, and one IPv6 entry stopped the rest of the sync.) A sync is one batch
11
+ * (iptables-restore / nft -f), not one process per address.
7
12
  */
8
13
 
9
- const { execSync } = require('child_process');
14
+ const { execFileSync } = require('child_process');
15
+ const net = require('net');
10
16
  const os = require('os');
11
17
 
12
18
  const CHAIN_NAME = 'SECURENOW_BLOCK';
19
+ const NFT_TABLE = 'securenow';
13
20
 
14
21
  let _options = null;
15
22
  let _active = false;
16
- let _useNft = false;
23
+ let _backend = null; // 'iptables' | 'nft'
24
+ let _ip6 = false; // ip6tables available alongside iptables
17
25
 
18
- function exec(cmd) {
19
- return execSync(cmd, { stdio: 'pipe', timeout: 10000 }).toString().trim();
26
+ function run(cmd, args, input) {
27
+ return execFileSync(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], timeout: 15000, input }).toString().trim();
20
28
  }
21
29
 
22
- function canRun(cmd) {
23
- try { exec(cmd); return true; } catch { return false; }
30
+ function canRun(cmd, args) {
31
+ try { run(cmd, args); return true; } catch { return false; }
32
+ }
33
+
34
+ function log(method, ...args) {
35
+ if (_options && _options.log) console[method](...args);
36
+ }
37
+
38
+ // A literal IPv4/IPv6 address or CIDR, or null.
39
+ function parseEntry(raw) {
40
+ const entry = String(raw == null ? '' : raw).trim();
41
+ const slash = entry.indexOf('/');
42
+ const addr = slash === -1 ? entry : entry.slice(0, slash);
43
+ const family = net.isIP(addr);
44
+ if (!family) return null;
45
+ if (slash !== -1) {
46
+ const bits = entry.slice(slash + 1);
47
+ if (!/^\d{1,3}$/.test(bits) || Number(bits) > (family === 4 ? 32 : 128)) return null;
48
+ }
49
+ return { family, value: entry };
50
+ }
51
+
52
+ function splitFamilies(ips) {
53
+ const v4 = [];
54
+ const v6 = [];
55
+ let rejected = 0;
56
+ for (const raw of ips || []) {
57
+ const parsed = parseEntry(raw);
58
+ if (!parsed) { rejected++; continue; }
59
+ (parsed.family === 4 ? v4 : v6).push(parsed.value);
60
+ }
61
+ return { v4, v6, rejected };
24
62
  }
25
63
 
26
64
  function detectBackend() {
27
- if (canRun('nft --version')) return 'nft';
28
- if (canRun('iptables --version')) return 'iptables';
65
+ // iptables first: iptables-legacy and iptables-nft both accept it, while bare nft rules depend on
66
+ // tables the host may not have.
67
+ if (canRun('iptables', ['--version'])) return 'iptables';
68
+ if (canRun('nft', ['--version'])) return 'nft';
29
69
  return null;
30
70
  }
31
71
 
32
- // ────── iptables backend ──────
72
+ // ────── iptables / ip6tables backend ──────
33
73
 
34
- function iptablesSetup() {
35
- try { exec(`iptables -N ${CHAIN_NAME}`); } catch (_) {} // chain may already exist
36
- try { exec(`iptables -C INPUT -j ${CHAIN_NAME}`); } catch (_) {
37
- exec(`iptables -I INPUT 1 -j ${CHAIN_NAME}`);
74
+ function iptablesSetup(bin) {
75
+ try { run(bin, ['-N', CHAIN_NAME]); } catch (_) {} // chain may already exist
76
+ try { run(bin, ['-C', 'INPUT', '-j', CHAIN_NAME]); } catch (_) {
77
+ run(bin, ['-I', 'INPUT', '1', '-j', CHAIN_NAME]);
38
78
  }
39
79
  }
40
80
 
41
- function iptablesSync(ips) {
42
- exec(`iptables -F ${CHAIN_NAME}`);
43
- for (const ip of ips) {
44
- exec(`iptables -A ${CHAIN_NAME} -s ${ip} -j DROP`);
45
- }
81
+ // Input for iptables-restore --noflush: replaces the chain's rules in one step, leaves everything else.
82
+ function restoreScript(entries) {
83
+ return [
84
+ '*filter',
85
+ `:${CHAIN_NAME} - [0:0]`,
86
+ `-F ${CHAIN_NAME}`,
87
+ ...entries.map((entry) => `-A ${CHAIN_NAME} -s ${entry} -j DROP`),
88
+ 'COMMIT',
89
+ '',
90
+ ].join('\n');
46
91
  }
47
92
 
48
- function iptablesCleanup() {
49
- try { exec(`iptables -D INPUT -j ${CHAIN_NAME}`); } catch (_) {}
50
- try { exec(`iptables -F ${CHAIN_NAME}`); } catch (_) {}
51
- try { exec(`iptables -X ${CHAIN_NAME}`); } catch (_) {}
93
+ function iptablesSync(v4, v6) {
94
+ run('iptables-restore', ['--noflush'], restoreScript(v4));
95
+ if (_ip6) run('ip6tables-restore', ['--noflush'], restoreScript(v6));
52
96
  }
53
97
 
54
- // ────── nftables backend ──────
98
+ function iptablesCleanup(bin) {
99
+ try { run(bin, ['-D', 'INPUT', '-j', CHAIN_NAME]); } catch (_) {}
100
+ try { run(bin, ['-F', CHAIN_NAME]); } catch (_) {}
101
+ try { run(bin, ['-X', CHAIN_NAME]); } catch (_) {}
102
+ }
55
103
 
56
- function nftSetup() {
57
- try { exec(`nft list chain ip filter ${CHAIN_NAME}`); } catch (_) {
58
- exec(`nft add chain ip filter ${CHAIN_NAME}`);
59
- }
60
- try { exec(`nft insert rule ip filter INPUT jump ${CHAIN_NAME}`); } catch (_) {}
104
+ // ────── nftables backend (own table, own input hook) ──────
105
+
106
+ // One nft -f transaction: make sure the table exists, drop it, add it back with the current rules.
107
+ function nftScript(v4, v6) {
108
+ return [
109
+ `table inet ${NFT_TABLE}`,
110
+ `delete table inet ${NFT_TABLE}`,
111
+ `table inet ${NFT_TABLE} {`,
112
+ ' chain input {',
113
+ ' type filter hook input priority -10; policy accept;',
114
+ ...v4.map((entry) => ` ip saddr ${entry} drop`),
115
+ ...v6.map((entry) => ` ip6 saddr ${entry} drop`),
116
+ ' }',
117
+ '}',
118
+ '',
119
+ ].join('\n');
61
120
  }
62
121
 
63
- function nftSync(ips) {
64
- exec(`nft flush chain ip filter ${CHAIN_NAME}`);
65
- for (const ip of ips) {
66
- exec(`nft add rule ip filter ${CHAIN_NAME} ip saddr ${ip} drop`);
67
- }
122
+ function nftSync(v4, v6) {
123
+ run('nft', ['-f', '-'], nftScript(v4, v6));
68
124
  }
69
125
 
70
126
  function nftCleanup() {
71
- try { exec(`nft flush chain ip filter ${CHAIN_NAME}`); } catch (_) {}
72
- try { exec(`nft delete chain ip filter ${CHAIN_NAME}`); } catch (_) {}
127
+ try { run('nft', ['delete', 'table', 'inet', NFT_TABLE]); } catch (_) {}
73
128
  }
74
129
 
75
130
  // ────── Public API ──────
76
131
 
77
- let _syncCallback = null;
78
-
79
132
  function init(options) {
80
133
  _options = options;
81
134
 
82
135
  if (os.platform() !== 'linux') {
83
- if (_options.log) console.warn('[securenow] Firewall iptables: only supported on Linux, skipping');
136
+ log('warn', '[securenow] Firewall iptables: only supported on Linux, skipping');
84
137
  return;
85
138
  }
86
139
 
87
- const backend = detectBackend();
88
- if (!backend) {
89
- if (_options.log) console.warn('[securenow] Firewall iptables: neither iptables nor nft found, skipping');
140
+ _backend = detectBackend();
141
+ if (!_backend) {
142
+ log('warn', '[securenow] Firewall iptables: neither iptables nor nft found, skipping');
90
143
  return;
91
144
  }
92
145
 
93
- _useNft = backend === 'nft';
94
- const label = _useNft ? 'nftables' : 'iptables';
95
-
96
146
  try {
97
- if (_useNft) nftSetup(); else iptablesSetup();
147
+ if (_backend === 'nft') {
148
+ nftSync([], []);
149
+ } else {
150
+ iptablesSetup('iptables');
151
+ if (canRun('ip6tables', ['--version'])) {
152
+ try { iptablesSetup('ip6tables'); _ip6 = true; } catch (_) { _ip6 = false; }
153
+ }
154
+ }
98
155
  _active = true;
99
- if (_options.log) console.log('[securenow] Firewall iptables: %s chain %s ready', label, CHAIN_NAME);
156
+ log('log', '[securenow] Firewall iptables: %s %s ready%s', _backend === 'nft' ? 'nftables table' : 'iptables chain',
157
+ _backend === 'nft' ? NFT_TABLE : CHAIN_NAME, _backend === 'iptables' && !_ip6 ? ' (IPv4 only: no ip6tables)' : '');
100
158
  } catch (e) {
101
- if (_options.log) console.warn('[securenow] Firewall iptables: setup failed (need root or CAP_NET_ADMIN):', e.message);
159
+ log('warn', '[securenow] Firewall iptables: setup failed (need root or CAP_NET_ADMIN):', e.message);
102
160
  }
103
161
  }
104
162
 
@@ -110,30 +168,33 @@ function sync(ips) {
110
168
  if (!_active) return;
111
169
 
112
170
  const maxRules = (_options && _options.iptablesMax) || 10000;
113
- const limited = ips.length > maxRules ? ips.slice(0, maxRules) : ips;
114
-
115
- if (ips.length > maxRules && _options.log) {
116
- console.warn('[securenow] Firewall iptables: truncated to %d rules (max %d)', maxRules, maxRules);
117
- }
171
+ const list = Array.isArray(ips) ? ips : [];
172
+ if (list.length > maxRules) log('warn', '[securenow] Firewall iptables: truncated to %d rules (max %d)', maxRules, maxRules);
173
+ const { v4, v6, rejected } = splitFamilies(list.slice(0, maxRules));
174
+ if (rejected) log('warn', '[securenow] Firewall iptables: ignored %d blocklist entries that are not IPs or CIDRs', rejected);
118
175
 
119
176
  try {
120
- if (_useNft) nftSync(limited); else iptablesSync(limited);
177
+ if (_backend === 'nft') nftSync(v4, v6);
178
+ else iptablesSync(v4, v6);
121
179
  } catch (e) {
122
- if (_options && _options.log) {
123
- console.warn('[securenow] Firewall iptables: sync failed:', e.message);
124
- }
180
+ log('warn', '[securenow] Firewall iptables: sync failed:', e.message);
125
181
  }
126
182
  }
127
183
 
128
184
  function shutdown() {
129
185
  if (!_active) return;
130
186
  try {
131
- if (_useNft) nftCleanup(); else iptablesCleanup();
132
- if (_options && _options.log) console.log('[securenow] Firewall iptables: chain %s cleaned up', CHAIN_NAME);
187
+ if (_backend === 'nft') {
188
+ nftCleanup();
189
+ } else {
190
+ iptablesCleanup('iptables');
191
+ if (_ip6) iptablesCleanup('ip6tables');
192
+ }
193
+ log('log', '[securenow] Firewall iptables: %s cleaned up', _backend === 'nft' ? `table ${NFT_TABLE}` : `chain ${CHAIN_NAME}`);
133
194
  } catch (e) {
134
- if (_options && _options.log) console.warn('[securenow] Firewall iptables: cleanup failed:', e.message);
195
+ log('warn', '[securenow] Firewall iptables: cleanup failed:', e.message);
135
196
  }
136
197
  _active = false;
137
198
  }
138
199
 
139
- module.exports = { init, sync, shutdown };
200
+ module.exports = { init, sync, shutdown, _test: { parseEntry, splitFamilies, restoreScript, nftScript } };
package/firewall-only.js CHANGED
@@ -41,6 +41,8 @@ if (firewallOptions.apiKey) {
41
41
  aws: firewallOptions.aws,
42
42
  gcp: firewallOptions.gcp,
43
43
  });
44
+ // Zero-code session revocation / session-theft capture (config.sessions.guard / .capture).
45
+ require('./sessions').installHttpGuard(appConfig.resolveSessionsOptions());
44
46
 
45
47
  const shutdown = () => { try { require('./firewall').shutdown(); } catch (_) {} };
46
48
  process.on('SIGINT', shutdown);
package/firewall-tcp.js CHANGED
@@ -18,12 +18,20 @@ let _options = null;
18
18
  let _patched = false;
19
19
  const _origListen = net.Server.prototype.listen;
20
20
 
21
+ const TCP_CHECKED = Symbol('securenow.tcpChecked');
22
+
23
+ // Returns true when it destroyed the socket. The firewall's emit guard calls it on every 'connection'
24
+ // of an HTTP/HTTPS/HTTP2 server, including servers that were already listening when the firewall
25
+ // started (the app usually calls listen() before the SDK's asynchronous start completes, so a listen()
26
+ // patch alone never saw them); the listen() patch below covers other net servers created later.
21
27
  function onConnection(socket) {
28
+ if (!socket || socket[TCP_CHECKED] || !_getMatcher) return false;
29
+ socket[TCP_CHECKED] = true;
22
30
  const ip = resolveSocketIp(socket);
23
31
 
24
32
  // Skip if the connection is from a trusted proxy — Layer 1 will handle it
25
33
  // with proper X-Forwarded-For resolution
26
- if (isFromTrustedProxy(ip) || isFromTrustedProxy('::ffff:' + ip)) return;
34
+ if (isFromTrustedProxy(ip) || isFromTrustedProxy('::ffff:' + ip)) return false;
27
35
 
28
36
  // Allowlist check: if active, only listed IPs pass
29
37
  const allowlistMatcher = _getAllowlistMatcher ? _getAllowlistMatcher() : null;
@@ -33,21 +41,23 @@ function onConnection(socket) {
33
41
  console.log('[securenow] Firewall: blocked %s via TCP (not in allowlist)', ip);
34
42
  }
35
43
  socket.destroy();
36
- return;
44
+ return true;
37
45
  }
38
- return; // on allowlist — allow
46
+ return false; // on allowlist — allow
39
47
  }
40
48
 
41
49
  // Blocklist check
42
50
  const matcher = _getMatcher();
43
- if (!matcher) return;
51
+ if (!matcher) return false;
44
52
 
45
53
  if (matcher.isBlocked(ip)) {
46
54
  if (_options && _options.log) {
47
55
  console.log('[securenow] Firewall: blocked %s via TCP (socket destroyed)', ip);
48
56
  }
49
57
  socket.destroy();
58
+ return true;
50
59
  }
60
+ return false;
51
61
  }
52
62
 
53
63
  function init(getMatcher, options, getAllowlistMatcher) {
@@ -71,4 +81,8 @@ function shutdown() {
71
81
  }
72
82
  }
73
83
 
74
- module.exports = { init, shutdown };
84
+ function checkConnection(socket) {
85
+ return _patched ? onConnection(socket) : false;
86
+ }
87
+
88
+ module.exports = { init, shutdown, checkConnection };
package/firewall.js CHANGED
@@ -1445,12 +1445,31 @@ const _origHttpsEmit = https.Server.prototype.emit;
1445
1445
  const FIREWALL_CHECKED = Symbol('securenow.firewallChecked');
1446
1446
  let _emitPatched = false;
1447
1447
 
1448
+ // Checks that run after the firewall lets a request through, independent of its remote toggle (the
1449
+ // zero-code session guard in sessions.js). Each returns true when it has answered the request itself.
1450
+ const _requestHooks = [];
1451
+ // Layer 2, when enabled: checked on each 'connection', before the server parses anything.
1452
+ let _tcpLayer = null;
1453
+ function addRequestHook(fn) {
1454
+ if (typeof fn === 'function' && !_requestHooks.includes(fn)) _requestHooks.push(fn);
1455
+ }
1456
+ function runRequestHooks(req, res) {
1457
+ for (const hook of _requestHooks) {
1458
+ try {
1459
+ if (hook(req, res)) return true;
1460
+ } catch (_) {}
1461
+ }
1462
+ return false;
1463
+ }
1464
+
1448
1465
  // One 'request' guard for every server kind: the firewall runs once per request, then the server's own emit.
1449
1466
  function guardedEmit(originalEmit) {
1450
1467
  return function securenowGuardedEmit(event, req, res, ...rest) {
1468
+ if (event === 'connection' && _tcpLayer && req && _tcpLayer.checkConnection(req)) return true;
1451
1469
  if (event === 'request' && req && res && !res.headersSent && !req[FIREWALL_CHECKED]) {
1452
1470
  req[FIREWALL_CHECKED] = true;
1453
1471
  if (firewallRequestHandler(req, res)) return true;
1472
+ if (_requestHooks.length && runRequestHooks(req, res)) return true;
1454
1473
  }
1455
1474
  return originalEmit.call(this, event, req, res, ...rest);
1456
1475
  };
@@ -1528,6 +1547,7 @@ function init(options) {
1528
1547
  const tcpLayer = require('./firewall-tcp');
1529
1548
  tcpLayer.init(() => _matcher, _options, () => _allowlistMatcher);
1530
1549
  _layers.push(tcpLayer);
1550
+ _tcpLayer = tcpLayer;
1531
1551
  if (_options.log) fwLog('[securenow] Firewall: Layer 2 (TCP drop) active');
1532
1552
  } catch (e) {
1533
1553
  if (_options.log) fwWarn('[securenow] Firewall: Layer 2 (TCP drop) failed:', e.message);
@@ -1671,4 +1691,4 @@ const _test = {
1671
1691
  identity: () => ({ sdkVersion: SDK_VERSION, instanceId: INSTANCE_ID }),
1672
1692
  };
1673
1693
 
1674
- module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, getBlocklistRules, getRateLimitRules, getChallengeRules, isRemoteEnabled, _test };
1694
+ module.exports = { init, shutdown, addRequestHook, getStats, getMatcher, getAllowlistMatcher, getBlocklistRules, getRateLimitRules, getChallengeRules, isRemoteEnabled, _test };
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Route templates (http.route) for frameworks the OpenTelemetry bundle does not instrument: Hono, h3,
5
+ * Feathers and Polka. Without these their request spans carried only the raw path (/users/42), not the
6
+ * route (/users/:id). Each hook reads the framework's own routing result and writes it to the request's
7
+ * server span; nothing changes how the app routes or responds.
8
+ *
9
+ * The same hooks record a handler's server error (5xx) as an exception event, with its stack, on that
10
+ * span: these frameworks catch the error and answer 500 themselves, so it never reached the HTTP layer.
11
+ *
12
+ * One OpenTelemetry instrumentation, so CommonJS and ESM (through the preload's ESM hook) are both patched.
13
+ */
14
+
15
+ const { InstrumentationBase, InstrumentationNodeModuleDefinition } = require('@opentelemetry/instrumentation');
16
+
17
+ const PATCHED = Symbol.for('securenow.routePatched');
18
+
19
+ // The route goes on the request's server span twice: through the RPC metadata the HTTP instrumentation
20
+ // reads when the response finishes (it then names the span "GET /users/:id"), and as an attribute now.
21
+ function setRoute(route) {
22
+ if (!route || typeof route !== 'string') return;
23
+ try {
24
+ const api = require('@opentelemetry/api');
25
+ const core = require('@opentelemetry/core');
26
+ const meta = core.getRPCMetadata(api.context.active());
27
+ if (meta && meta.type === core.RPCType.HTTP) meta.route = route;
28
+ const span = api.trace.getActiveSpan();
29
+ if (span) span.setAttribute('http.route', route);
30
+ } catch (_) {}
31
+ }
32
+
33
+ function markPatched(fn) {
34
+ try { Object.defineProperty(fn, PATCHED, { value: true }); } catch (_) {}
35
+ return fn;
36
+ }
37
+
38
+ // The HTTP status an error stands for: statusCode (h3), status (Hono HTTPException, http-errors), or a
39
+ // numeric code (Feathers errors). Anything else is a 500.
40
+ function errorStatus(err) {
41
+ if (!err || typeof err !== 'object') return 500;
42
+ const status = Number(err.statusCode || err.status || (typeof err.code === 'number' ? err.code : 0));
43
+ return Number.isInteger(status) && status >= 100 && status < 600 ? status : 500;
44
+ }
45
+
46
+ // A 4xx is the client's doing (not found, unauthorized...) and stays off the span, as in OpenTelemetry's
47
+ // HTTP conventions; the status code itself still records it.
48
+ function recordServerError(err, status = errorStatus(err)) {
49
+ if (!err || status < 500) return;
50
+ try {
51
+ const span = require('@opentelemetry/api').trace.getActiveSpan();
52
+ if (span) span.recordException(err instanceof Error ? err : String(err && err.message ? err.message : err));
53
+ } catch (_) {}
54
+ }
55
+
56
+ // ---- Hono 4: a '*' middleware added to every app reads the route that answered, after the handler ran.
57
+ function honoRouteOf(c) {
58
+ const req = c && c.req;
59
+ if (!req) return null;
60
+ try {
61
+ if (typeof req.routePath === 'string') return req.routePath;
62
+ } catch (_) {}
63
+ try {
64
+ const routes = req.matchedRoutes;
65
+ if (Array.isArray(routes) && routes[req.routeIndex]) return routes[req.routeIndex].path;
66
+ } catch (_) {}
67
+ return null;
68
+ }
69
+
70
+ function patchHono(exports) {
71
+ const Original = exports && exports.Hono;
72
+ if (typeof Original !== 'function' || Original[PATCHED]) return exports;
73
+ class Hono extends Original {
74
+ constructor(...args) {
75
+ super(...args);
76
+ try {
77
+ this.use('*', async function securenowRoute(c, next) {
78
+ try {
79
+ await next();
80
+ } finally {
81
+ const route = honoRouteOf(c);
82
+ if (route && route !== '*' && route !== '/*') setRoute(route);
83
+ // Hono's compose catches a handler's error, sets c.error and answers with app.onError.
84
+ if (c && c.error) recordServerError(c.error);
85
+ }
86
+ });
87
+ } catch (_) {}
88
+ }
89
+ }
90
+ try { Object.defineProperty(Hono, 'name', { value: 'Hono' }); } catch (_) {}
91
+ markPatched(Hono);
92
+ try { exports.Hono = Hono; } catch (_) {}
93
+ return exports;
94
+ }
95
+
96
+ // ---- h3 1: the router records event.context.matchedRoute; an onBeforeResponse hook reads it.
97
+ function patchH3(exports) {
98
+ const original = exports && exports.createApp;
99
+ if (typeof original !== 'function' || original[PATCHED]) return exports;
100
+ const createApp = markPatched(function createApp(options) {
101
+ const opts = options || {};
102
+ const userHook = opts.onBeforeResponse;
103
+ const userOnError = opts.onError;
104
+ return original.call(this, {
105
+ ...opts,
106
+ onBeforeResponse(event, response) {
107
+ try {
108
+ const matched = event && event.context && event.context.matchedRoute;
109
+ if (matched && matched.path) setRoute(matched.path);
110
+ } catch (_) {}
111
+ return typeof userHook === 'function' ? userHook.call(this, event, response) : undefined;
112
+ },
113
+ // h3 wraps a thrown error in an H3Error (cause = the original, with its stack), calls onError, then
114
+ // still sends its default error response unless this hook handled the event.
115
+ onError(error, event) {
116
+ try {
117
+ recordServerError(error && error.cause instanceof Error ? error.cause : error, errorStatus(error));
118
+ } catch (_) {}
119
+ return typeof userOnError === 'function' ? userOnError.call(this, error, event) : undefined;
120
+ },
121
+ });
122
+ });
123
+ try { exports.createApp = createApp; } catch (_) {}
124
+ return exports;
125
+ }
126
+
127
+ // ---- Feathers 5: an app-wide around hook names external service calls (/users, /users/:id).
128
+ function feathersRoute(context) {
129
+ if (!context || typeof context.path !== 'string') return null;
130
+ const base = `/${context.path.replace(/^\/+/, '')}`;
131
+ return context.id !== undefined && context.id !== null ? `${base}/:id` : base;
132
+ }
133
+
134
+ function patchFeathers(exports) {
135
+ const original = exports && exports.feathers;
136
+ if (typeof original !== 'function' || original[PATCHED]) return exports;
137
+ const feathers = markPatched(function feathers(...args) {
138
+ const app = original.apply(this, args);
139
+ try {
140
+ app.hooks({
141
+ around: {
142
+ all: [async function securenowRoute(context, next) {
143
+ try {
144
+ await next();
145
+ } catch (err) {
146
+ if (context && context.params && context.params.provider) recordServerError(err);
147
+ throw err;
148
+ } finally {
149
+ // Only calls that came in through a transport (REST, sockets), not service-to-service calls.
150
+ if (context && context.params && context.params.provider) {
151
+ const route = feathersRoute(context);
152
+ if (route) setRoute(route);
153
+ }
154
+ }
155
+ }],
156
+ },
157
+ });
158
+ } catch (_) {}
159
+ return app;
160
+ });
161
+ try { exports.feathers = feathers; } catch (_) {}
162
+ return exports;
163
+ }
164
+
165
+ // ---- Polka (trouter 2): find() returns the matched route's handlers; their registered pattern is the route.
166
+ // Polka passes next(err) errors to the app's own onError, which find() wraps the first time it runs.
167
+ function wrapPolkaOnError(app) {
168
+ const userOnError = app && app.onError;
169
+ if (typeof userOnError !== 'function' || userOnError[PATCHED]) return;
170
+ app.onError = markPatched(function onError(err, ...rest) {
171
+ recordServerError(err);
172
+ return userOnError.call(this, err, ...rest);
173
+ });
174
+ }
175
+
176
+ function patchTrouter(exports) {
177
+ const Trouter = exports && (typeof exports === 'function' ? exports : exports.default);
178
+ const proto = Trouter && Trouter.prototype;
179
+ if (!proto || typeof proto.find !== 'function' || proto.find[PATCHED]) return exports;
180
+ const original = proto.find;
181
+ proto.find = markPatched(function find(method, url) {
182
+ const result = original.call(this, method, url);
183
+ try { wrapPolkaOnError(this); } catch (_) {}
184
+ try {
185
+ if (result && result.handlers && this.handlers) {
186
+ for (const m of [method, '*']) {
187
+ const table = this.handlers[m];
188
+ if (!table) continue;
189
+ for (const pattern of Object.keys(table)) {
190
+ if (table[pattern] === result.handlers) {
191
+ setRoute(pattern);
192
+ return result;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ } catch (_) {}
198
+ return result;
199
+ });
200
+ return exports;
201
+ }
202
+
203
+ class SecureNowRouteInstrumentation extends InstrumentationBase {
204
+ constructor(config = {}) {
205
+ super('securenow-framework-routes', '1.0.0', config);
206
+ }
207
+
208
+ init() {
209
+ return [
210
+ new InstrumentationNodeModuleDefinition('hono', ['>=4.0.0 <5'], patchHono),
211
+ new InstrumentationNodeModuleDefinition('h3', ['>=1.0.0 <2'], patchH3),
212
+ new InstrumentationNodeModuleDefinition('@feathersjs/feathers', ['>=5.0.0 <6'], patchFeathers),
213
+ new InstrumentationNodeModuleDefinition('trouter', ['>=2.0.0 <3'], patchTrouter),
214
+ ];
215
+ }
216
+ }
217
+
218
+ module.exports = {
219
+ SecureNowRouteInstrumentation,
220
+ // For tests.
221
+ _test: { setRoute, patchHono, patchH3, patchFeathers, patchTrouter, honoRouteOf, feathersRoute, errorStatus, recordServerError },
222
+ };
package/log-format.js CHANGED
@@ -19,6 +19,8 @@ function renderArg(arg) {
19
19
 
20
20
  function formatLogArgs(args) {
21
21
  if (!Array.isArray(args) || args.length === 0) return '';
22
+ // Credential fields in logged objects are redacted first (config.logging.redact, log-redaction.js).
23
+ try { args = require('./log-redaction').redactLogArgs(args); } catch (_) { /* format as is */ }
22
24
  if (typeof args[0] === 'string' && PLACEHOLDER.test(args[0])) {
23
25
  try { return util.format(...args); } catch (_) { /* fall through */ }
24
26
  }