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.
package/register.d.ts CHANGED
@@ -1,61 +1,54 @@
1
1
  /**
2
2
  * SecureNow Register Module TypeScript Declarations
3
- *
4
- * Main entry point for Node.js/Express/Fastify/NestJS applications.
5
- * This module is automatically loaded when using NODE_OPTIONS="-r securenow/register"
3
+ *
4
+ * Preload for Node.js servers (Express, Fastify, NestJS, Koa, Hapi, Hono, Polka, Feathers, h3,
5
+ * raw node:http): `node -r securenow/register <entry>`. It starts tracing, logs, body capture,
6
+ * the firewall and (when configured) session revocation before the app loads.
6
7
  */
7
8
 
8
9
  /**
9
- * Configuration:
10
- *
11
- * Local development reads .securenow/credentials.json. Run
12
- * `npx securenow login` to choose/create an app, then `npx securenow init`
13
- * to ensure secure defaults and explanations are present.
14
- *
15
- * Production uses the same file shape. Run
16
- * `npx securenow credentials runtime --env production`, then mount/copy the
17
- * generated JSON to .securenow/credentials.json in the running app.
10
+ * Configuration: .securenow/runtime.json, written by `npx securenow login`; `npx securenow init`
11
+ * adds the secure defaults with explanations.
18
12
  *
19
- * Legacy env vars are fallback-only for existing installs.
13
+ * Production: `npx securenow credentials runtime --env production`, then mount/copy the file to
14
+ * .securenow/credentials.json in the running app, or store the `--format env` output
15
+ * (SECURENOW_CREDENTIALS) as a secret environment variable on hosts without file mounts.
16
+ * SECURENOW_DISABLED=1 turns the SDK off (tests).
20
17
  */
21
18
 
22
19
  // This module has side effects (initializes OpenTelemetry)
23
- // It's typically used via NODE_OPTIONS, not imported directly
20
+ // It's typically preloaded with -r, not imported
24
21
  export {};
25
22
 
26
23
  /**
27
24
  * @example
28
25
  * ```bash
29
- * # Using with Node.js preload
30
- * NODE_OPTIONS="-r securenow/register" node app.js
26
+ * # Preload (CommonJS, ESM and TypeScript alike)
27
+ * node -r securenow/register app.js
28
+ * node -r securenow/register --import tsx src/index.ts
29
+ * NODE_OPTIONS="-r securenow/register" npm start
31
30
  * ```
32
- *
33
- * @example
34
- * ```bash
35
- * # Using with PM2
36
- * pm2 start app.js --node-args="-r securenow/register"
37
- * ```
38
- *
31
+ *
39
32
  * @example
40
33
  * ```javascript
41
- * // ecosystem.config.js
34
+ * // ecosystem.config.cjs (PM2, fork or cluster mode)
42
35
  * module.exports = {
43
36
  * apps: [{
44
37
  * name: 'my-app',
45
38
  * script: './app.js',
46
- * node_args: '-r securenow/register',
47
- * // Local and production use .securenow/credentials.json.
39
+ * // An absolute path: a bare '-r securenow/register' is resolved from the PM2 daemon's
40
+ * // folder in cluster mode, and the workers crash-loop with "Cannot find module".
41
+ * node_args: ['-r', require.resolve('securenow/register')],
42
+ * instances: 'max',
43
+ * exec_mode: 'cluster',
48
44
  * }]
49
45
  * };
50
46
  * ```
51
- *
47
+ *
52
48
  * @example
53
- * ```typescript
54
- * // Explicit import (optional, mainly for side effects)
55
- * import 'securenow/register';
56
- *
57
- * import express from 'express';
58
- * const app = express();
59
- * // Your app code...
49
+ * ```javascript
50
+ * // First line of a CommonJS entry (alternative to -r)
51
+ * require('securenow/register');
52
+ * const express = require('express');
60
53
  * ```
61
54
  */
@@ -44,18 +44,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
44
44
  // and they only ever transform captured telemetry strings (read-only) — never
45
45
  // the actual request/response stream. Compiled once at module load.
46
46
  const VALUE_REDACTORS = [
47
+ // The JWT and email patterns only start where a run of their characters starts (the
48
+ // lookbehind), so a long run that holds no match is scanned once, not once per position:
49
+ // a crafted 10KB value costs microseconds, not 50ms+ of event loop.
47
50
  // JWT: three base64url segments. Anchored to the eyJ header so it won't
48
51
  // match arbitrary dotted tokens.
49
- { name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
52
+ { name: 'jwt', hint: 'eyJ', re: /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
50
53
  // Bearer/Basic auth header value embedded in a body string.
51
54
  { name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
52
55
  // Stripe-style / SecureNow live+test API keys.
53
56
  { name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
54
- // Email addresses (bounded local/domain parts).
55
- { name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
57
+ // Email addresses (bounded local/domain parts: 64 and 253 characters at most).
58
+ { name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
56
59
  // Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
57
60
  // space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
58
- { name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
61
+ { name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
59
62
  ];
60
63
 
61
64
  // Skip the value-shape scan on very large strings to bound per-body cost.
@@ -77,15 +80,17 @@ function luhnValid(digits) {
77
80
  /**
78
81
  * Redact obvious secret/PII shapes inside a captured string VALUE.
79
82
  * Returns the input unchanged when nothing matches (cheap common case).
83
+ * maxScanLength: longer values skip the shape scan (log lines pass a larger bound).
80
84
  */
81
- function redactSensitiveValue(value) {
85
+ function redactSensitiveValue(value, maxScanLength = MAX_VALUE_SCAN_LENGTH) {
82
86
  if (typeof value !== 'string' || value.length === 0) return value;
83
87
  // Credential assignments in GraphQL/query strings need field-aware
84
88
  // redaction even though a short numeric OTP has no unique value shape.
85
89
  value = redactOtpQuery(redactOtpGraphQL(value));
86
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
90
+ if (value.length > maxScanLength) return value; // bound shape scans
87
91
  let out = value;
88
92
  for (const r of VALUE_REDACTORS) {
93
+ if (r.hint && !out.includes(r.hint)) continue;
89
94
  r.re.lastIndex = 0;
90
95
  if (r.luhn) {
91
96
  out = out.replace(r.re, (m) => {
package/resolve-ip.js CHANGED
@@ -7,17 +7,19 @@ const { createMatcher } = require('./cidr');
7
7
  const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
8
8
  const PRIVATE_IP_RE = /^(127\.|::1$|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|f[cd][0-9a-f]{2}:)/i;
9
9
 
10
- // Named proxy presets for config.networking.trustedProxies. Cloudflare's published IPv4 edge ranges
11
- // (https://www.cloudflare.com/ips-v4): an app behind Cloudflare trusts them with "cloudflare".
10
+ // Named proxy presets for config.networking.trustedProxies. Cloudflare's published edge ranges
11
+ // (https://www.cloudflare.com/ips-v4 and /ips-v6): an app behind Cloudflare trusts them with "cloudflare".
12
12
  const PROXY_PRESETS = {
13
13
  cloudflare: [
14
14
  '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18',
15
15
  '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17',
16
16
  '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
17
+ '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32',
18
+ '2a06:98c0::/29', '2c0f:f248::/32',
17
19
  ],
18
20
  };
19
21
 
20
- // Exact addresses (IPv4 or IPv6), IPv4 CIDR ranges and presets.
22
+ // Exact addresses and CIDR ranges (IPv4 or IPv6), and presets.
21
23
  function trustedProxyMatcher(entries) {
22
24
  const expanded = [];
23
25
  for (const entry of entries) {
@@ -93,14 +95,30 @@ function getHeader(headers, name) {
93
95
  return firstHeaderValue(headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()]);
94
96
  }
95
97
 
98
+ // A request that came over a Unix domain socket or a Windows named pipe has no remote address, and only a
99
+ // process on this machine can connect to one: nginx `proxy_pass http://unix:/run/app.sock`, Nuxt's dev
100
+ // proxy, a local sidecar. Such a connection is a trusted local proxy, so its forwarding headers count
101
+ // (before this, requests over a socket had no client IP at all and IP blocking never applied to them).
102
+ function arrivedOverLocalSocket(request) {
103
+ const socket = request && request.socket;
104
+ if (!socket || socket.remoteAddress) return false;
105
+ try {
106
+ const server = socket.server || socket._server;
107
+ return !!server && typeof server.address === 'function' && typeof server.address() === 'string';
108
+ } catch (_) {
109
+ return false;
110
+ }
111
+ }
112
+
96
113
  function resolveClientIpWithDetails(request) {
97
114
  const socketIp = normalizeIp(request.socket?.remoteAddress || '');
98
115
  const headers = request.headers || {};
99
- const trustedProxy = isFromTrustedProxy(socketIp);
116
+ const viaLocalSocket = !socketIp && arrivedOverLocalSocket(request);
117
+ const trustedProxy = viaLocalSocket || isFromTrustedProxy(socketIp);
100
118
 
101
119
  const details = {
102
120
  ip: socketIp,
103
- source: socketIp ? 'socket' : 'unknown',
121
+ source: socketIp ? 'socket' : (viaLocalSocket ? 'unix-socket' : 'unknown'),
104
122
  socketIp,
105
123
  trustedProxy,
106
124
  forwardedFor: String(getHeader(headers, 'x-forwarded-for') || ''),
package/sessions.d.ts CHANGED
@@ -30,6 +30,33 @@ export function guard(options?: GuardOptions): (req: any, res: any, next: any) =
30
30
  /** Express-style middleware that ONLY emits session.seen (detection feed, no enforcement). */
31
31
  export function capture(options?: GuardOptions): (req: any, res: any, next: any) => void;
32
32
 
33
+ export interface CheckOptions {
34
+ /** Extract the session id. Defaults to the hashed Bearer token or session cookie. */
35
+ getSessionId?: (req: any) => string | null | undefined;
36
+ /** Extract the user id. Defaults to the JWT `sub` claim. */
37
+ getUserId?: (req: any) => string | null | undefined;
38
+ /** Client IP for session.seen when `req` has no socket (a Web Request). */
39
+ ip?: string;
40
+ /** Emit session.seen for detection (default true). */
41
+ capture?: boolean;
42
+ }
43
+
44
+ /**
45
+ * For code that cannot use guard() (Fastify hooks, Koa/Hono middleware, Next.js route handlers, h3):
46
+ * is this request's session or user revoked? `req` is a Node request or a Web `Request`. Fail-open. (8.25.0+)
47
+ */
48
+ export function check(req: IncomingMessage | Request | { headers: any }, options?: CheckOptions): {
49
+ revoked: boolean;
50
+ sessionId: string | null;
51
+ userId: string | null;
52
+ };
53
+
54
+ /**
55
+ * Zero-code guard/capture for every framework, installed by the SDK from config.sessions.guard /
56
+ * config.sessions.capture in .securenow/runtime.json. Returns true when it installed the hook. (8.25.0+)
57
+ */
58
+ export function installHttpGuard(options?: { guard?: boolean; capture?: boolean }): boolean;
59
+
33
60
  /** Resolve the client IP from a request (X-Forwarded-For / CF / socket). */
34
61
  export function resolveClientIp(req: any): string | null;
35
62
 
package/sessions.js CHANGED
@@ -4,9 +4,10 @@
4
4
  // half of account-takeover response).
5
5
  //
6
6
  // const securenow = require('securenow/sessions');
7
- // app.use(securenow.guard()); // auto-detects NextAuth/connect.sid/JWT sessions
8
- // // ...or check manually in your auth middleware:
9
- // if (securenow.isRevoked({ sessionId, userId })) return res.status(401)...
7
+ // app.use(securenow.guard()); // Express-style; auto-detects NextAuth/connect.sid/JWT sessions
8
+ // // ...any other framework (Fastify, Koa, Hono, Next.js route handlers, h3):
9
+ // if (securenow.check(req).revoked) return /* a 401 */;
10
+ // // ...or no code at all: "sessions": { "guard": true, "capture": true } in .securenow/runtime.json
10
11
  //
11
12
  // The SDK pulls active revocations from SecureNow (outbound, ETag-cached) and
12
13
  // checks each request locally — no inbound webhook, works behind NAT, portable
@@ -150,6 +151,21 @@ const SESSION_COOKIES = [
150
151
  'sid',
151
152
  ];
152
153
 
154
+ // Session cookies: config.sessions.cookieNames first (the app's own), then the defaults.
155
+ function sessionCookieNames() {
156
+ let extra = [];
157
+ try { extra = appConfig.listConfig('sessions.cookieNames'); } catch {}
158
+ return extra.length ? [...extra, ...SESSION_COOKIES] : SESSION_COOKIES;
159
+ }
160
+
161
+ // A header from a Node request (plain object) or a Web Request (Headers).
162
+ function headerOf(req, name) {
163
+ const headers = req && req.headers;
164
+ if (!headers) return '';
165
+ if (typeof headers.get === 'function') return headers.get(name) || '';
166
+ return headers[name] || '';
167
+ }
168
+
153
169
  function parseCookies(header) {
154
170
  const out = {};
155
171
  if (!header) return out;
@@ -165,8 +181,7 @@ function parseCookies(header) {
165
181
  function defaultExtract(req) {
166
182
  let sessionId = null;
167
183
  let userId = null;
168
- const headers = req.headers || {};
169
- const auth = headers.authorization || headers.Authorization;
184
+ const auth = headerOf(req, 'authorization') || headerOf(req, 'Authorization');
170
185
  if (auth && /^Bearer\s+/i.test(auth)) {
171
186
  const token = auth.replace(/^Bearer\s+/i, '').trim();
172
187
  if (token) {
@@ -178,9 +193,10 @@ function defaultExtract(req) {
178
193
  } catch {}
179
194
  }
180
195
  }
181
- if (!sessionId && headers.cookie) {
182
- const cookies = parseCookies(headers.cookie);
183
- for (const name of SESSION_COOKIES) {
196
+ const cookieHeader = headerOf(req, 'cookie');
197
+ if (!sessionId && cookieHeader) {
198
+ const cookies = parseCookies(cookieHeader);
199
+ for (const name of sessionCookieNames()) {
184
200
  if (cookies[name]) {
185
201
  sessionId = sha256(cookies[name]);
186
202
  break;
@@ -255,7 +271,7 @@ function capture(options = {}) {
255
271
 
256
272
  function defaultOnRevoked(req, res) {
257
273
  try {
258
- const clears = SESSION_COOKIES.map((n) => `${n}=; Path=/; Max-Age=0; HttpOnly`);
274
+ const clears = sessionCookieNames().map((n) => `${n}=; Path=/; Max-Age=0; HttpOnly`);
259
275
  if (typeof res.setHeader === 'function') res.setHeader('Set-Cookie', clears);
260
276
  } catch {}
261
277
  if (typeof res.status === 'function' && typeof res.json === 'function') {
@@ -298,4 +314,49 @@ function guard(options = {}) {
298
314
  };
299
315
  }
300
316
 
301
- module.exports = { start, syncOnce, isRevoked, guard, capture, resolveClientIp, sha256 };
317
+ /**
318
+ * For code that cannot use guard() (Fastify hooks, Koa or Hono middleware, Next.js route handlers, h3):
319
+ * is this request's session or user revoked? `req` is a Node request or a Web Request. Feeds
320
+ * session-theft detection (session.seen) unless options.capture is false. Fail-open.
321
+ *
322
+ * @param {object} req
323
+ * @param {{ getSessionId?: (req)=>string, getUserId?: (req)=>string, ip?: string, capture?: boolean }} [options]
324
+ * @returns {{ revoked: boolean, sessionId: string|null, userId: string|null }}
325
+ */
326
+ function check(req, options = {}) {
327
+ try {
328
+ if (require('./sdk-disabled').sdkDisabled('sessions check')) return { revoked: false, sessionId: null, userId: null };
329
+ start(options);
330
+ const { sessionId, userId } = extractIdentity(req, options.getSessionId, options.getUserId);
331
+ if (options.capture !== false) emitSessionSeen(sessionId, userId, options.ip || resolveClientIp(req));
332
+ return { revoked: isRevoked({ sessionId, userId }), sessionId: sessionId || null, userId: userId || null };
333
+ } catch {
334
+ return { revoked: false, sessionId: null, userId: null };
335
+ }
336
+ }
337
+
338
+ // Zero code, every framework (config.sessions.guard / config.sessions.capture): the firewall's
339
+ // request hook runs this on each incoming request before the app sees it. guard answers a revoked
340
+ // session or user with a 401 and clears the session cookies; capture feeds session-theft detection.
341
+ let _httpGuardInstalled = false;
342
+ function installHttpGuard(options = {}) {
343
+ const enforce = !!options.guard;
344
+ const capture = !!options.capture;
345
+ if ((!enforce && !capture) || _httpGuardInstalled) return false;
346
+ if (require('./sdk-disabled').sdkDisabled('sessions')) return false;
347
+ _httpGuardInstalled = true;
348
+ start(options);
349
+ require('./firewall').addRequestHook(function securenowSessionHook(req, res) {
350
+ const { sessionId, userId } = defaultExtract(req);
351
+ if (!sessionId && !userId) return false;
352
+ if (capture) emitSessionSeen(sessionId, userId, resolveClientIp(req));
353
+ if (enforce && isRevoked({ sessionId, userId })) {
354
+ defaultOnRevoked(req, res);
355
+ return true;
356
+ }
357
+ return false;
358
+ });
359
+ return true;
360
+ }
361
+
362
+ module.exports = { start, syncOnce, isRevoked, guard, capture, check, installHttpGuard, resolveClientIp, sha256 };
package/tracing.d.ts CHANGED
@@ -1,74 +1,10 @@
1
1
  /**
2
2
  * SecureNow Tracing Module TypeScript Declarations
3
- *
4
- * Core tracing functionality for Node.js applications.
5
- * This is typically loaded via register.js, not imported directly.
6
- */
7
-
8
- /**
9
- * Default sensitive fields that are automatically redacted from request bodies
10
- */
11
- export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
12
-
13
- /**
14
- * Redact sensitive fields from an object (recursively)
15
- *
16
- * @param obj - Object to redact (can be nested)
17
- * @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
18
- * @returns Redacted copy of the object
19
- *
20
- * @example
21
- * ```typescript
22
- * import { redactSensitiveData } from 'securenow/tracing';
23
- *
24
- * const data = {
25
- * email: 'user@example.com',
26
- * password: 'secret123',
27
- * nested: {
28
- * api_key: 'sk_live_abc123'
29
- * }
30
- * };
31
- *
32
- * const redacted = redactSensitiveData(data);
33
- * // Result: {
34
- * // email: 'user@example.com',
35
- * // password: '[REDACTED]',
36
- * // nested: { api_key: '[REDACTED]' }
37
- * // }
38
- * ```
39
- */
40
- export function redactSensitiveData<T = any>(
41
- obj: T,
42
- sensitiveFields?: string[]
43
- ): T;
44
-
45
- /**
46
- * Redact sensitive data from GraphQL query strings
47
- *
48
- * @param query - GraphQL query string
49
- * @param sensitiveFields - Array of field names to redact
50
- * @returns Redacted query string
51
- *
52
- * @example
53
- * ```typescript
54
- * import { redactGraphQLQuery } from 'securenow/tracing';
55
- *
56
- * const query = `
57
- * mutation {
58
- * login(email: "user@example.com", password: "secret123") {
59
- * token
60
- * }
61
- * }
62
- * `;
63
- *
64
- * const redacted = redactGraphQLQuery(query);
65
- * // Result: mutation { login(email: "user@example.com", password: "[REDACTED]") { token } }
66
- * ```
3
+ *
4
+ * Core tracing for Node.js applications. It starts when loaded, usually through
5
+ * `node -r securenow/register`; import it directly only for the logger API below.
6
+ * Redaction helpers live in `securenow/nextjs-middleware` and the `securenow redact` CLI.
67
7
  */
68
- export function redactGraphQLQuery(
69
- query: string,
70
- sensitiveFields?: string[]
71
- ): string;
72
8
 
73
9
  /**
74
10
  * OpenTelemetry Logger interface
@@ -86,19 +22,20 @@ export interface LogRecord {
86
22
  * 5 = DEBUG, 9 = INFO, 13 = WARN, 17 = ERROR
87
23
  */
88
24
  severityNumber: number;
89
-
25
+
90
26
  /**
91
27
  * Human-readable severity text
92
28
  */
93
29
  severityText: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | string;
94
-
30
+
95
31
  /**
96
- * Log message body
32
+ * Log message body. Secret shapes (JWTs, Bearer credentials, API keys, card numbers, emails,
33
+ * OTP assignments) are redacted before export unless config.logging.redact is false.
97
34
  */
98
35
  body: string;
99
-
36
+
100
37
  /**
101
- * Structured attributes for filtering/searching
38
+ * Structured attributes for filtering/searching (exported as set)
102
39
  */
103
40
  attributes?: Record<string, any>;
104
41
  }
@@ -112,75 +49,57 @@ export interface LoggerProvider {
112
49
  }
113
50
 
114
51
  /**
115
- * Get a logger instance for sending structured logs to any OTLP-compatible backend
116
- *
117
- * @param name - Logger name (e.g., 'my-service', 'auth-module')
52
+ * Get a logger that sends structured logs to SecureNow, correlated with the active trace.
53
+ *
54
+ * @param name - Logger name (e.g., 'my-service', 'auth-module'); defaults to 'default'
118
55
  * @param version - Logger version (optional, defaults to '1.0.0')
119
- * @returns Logger instance or null if logging is not enabled
120
- *
56
+ * @returns Logger instance, or null when logging is off (config.logging.enabled=false) or the SDK
57
+ * did not start (SECURENOW_DISABLED=1)
58
+ *
121
59
  * @example
122
60
  * ```typescript
123
61
  * import { getLogger } from 'securenow/tracing';
124
- *
62
+ *
125
63
  * const logger = getLogger('my-service', '1.0.0');
126
- *
127
- * if (logger) {
128
- * logger.emit({
129
- * severityNumber: 9,
130
- * severityText: 'INFO',
131
- * body: 'User logged in',
132
- * attributes: {
133
- * userId: 123,
134
- * username: 'john',
135
- * },
136
- * });
137
- * }
64
+ *
65
+ * logger?.emit({
66
+ * severityNumber: 9,
67
+ * severityText: 'INFO',
68
+ * body: 'User logged in',
69
+ * attributes: { userId: 123 },
70
+ * });
138
71
  * ```
139
72
  */
140
73
  export function getLogger(name?: string, version?: string): Logger | null;
141
74
 
142
75
  /**
143
- * Check if logging is enabled
144
- *
145
- * @returns true when logging resolves enabled from .securenow/credentials.json defaults
146
- *
147
- * @example
148
- * ```typescript
149
- * import { isLoggingEnabled } from 'securenow/tracing';
150
- *
151
- * if (isLoggingEnabled()) {
152
- * console.log('Logging is enabled');
153
- * }
154
- * ```
76
+ * Whether logs are exported (config.logging.enabled, default true; false when the SDK did not start).
155
77
  */
156
78
  export function isLoggingEnabled(): boolean;
157
79
 
158
80
  /**
159
- * The OpenTelemetry LoggerProvider instance (if logging is enabled)
160
- * Use getLogger() instead of accessing this directly
81
+ * The OpenTelemetry LoggerProvider instance (null when logging is off).
82
+ * Use getLogger() instead of accessing this directly.
161
83
  */
162
84
  export const loggerProvider: LoggerProvider | null;
163
85
 
164
86
  /**
165
- * Configuration:
166
- *
167
- * Local development reads .securenow/credentials.json first. Run
168
- * `npx securenow login` to write app identity/runtime API key and
169
- * `npx securenow init` to ensure secure defaults and explanations.
87
+ * Configuration: .securenow/runtime.json, written by `npx securenow login`; `npx securenow init`
88
+ * adds the secure defaults with explanations.
170
89
  *
171
- * Production uses the same file shape. Run
172
- * `npx securenow credentials runtime --env production`, then mount/copy the
173
- * generated JSON to .securenow/credentials.json in the running app.
90
+ * Production: `npx securenow credentials runtime --env production`, then mount/copy the file to
91
+ * .securenow/credentials.json in the running app. Hosts without file mounts store
92
+ * `npx securenow credentials runtime --env production --format env` (SECURENOW_CREDENTIALS) as a
93
+ * secret environment variable. No other SECURENOW_* variable is read; SECURENOW_DISABLED=1
94
+ * turns the SDK off (tests).
174
95
  *
175
96
  * Main config fields:
176
- * - app.key / app.name
177
- * - config.otel.endpoint only for advanced/self-hosted collector overrides
178
- * - apiKey
97
+ * - app.key / app.name, apiKey (runtime API key)
179
98
  * - config.runtime.deploymentEnvironment
180
- * - config.logging.enabled
99
+ * - config.logging.enabled / redact
181
100
  * - config.capture.body / multipart / maxBodySize / sensitiveFields
182
- * - config.otel.endpoint / tracesEndpoint / logsEndpoint / headers
183
- * - config.firewall.enabled and layer/provider options
184
- *
185
- * Legacy env vars are fallback-only for existing installs.
101
+ * - config.otel.endpoint / tracesEndpoint / logsEndpoint / headers (self-hosted collectors only)
102
+ * - config.networking.trustedProxies
103
+ * - config.firewall.* (failMode, statusCode, tcp, iptables, cloud, rateLimitStore, ...)
104
+ * - config.sessions.guard / capture / cookieNames
186
105
  */
package/tracing.js CHANGED
@@ -1,9 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ // When the SDK does not start, the module still exports its API: getLogger() returns null, as it
4
+ // does with logging disabled, so app code that calls it keeps working under SECURENOW_DISABLED=1.
5
+ const NOT_STARTED = { loggerProvider: null, getLogger: () => null, isLoggingEnabled: () => false };
3
6
  // Do nothing inside Node's internal ESM-hooks thread (see internal-thread.js).
4
- if (require('./internal-thread').inInternalThread()) return;
7
+ if (require('./internal-thread').inInternalThread()) { module.exports = NOT_STARTED; return; }
5
8
  // SECURENOW_DISABLED=1 turns the SDK off for this process (test runs).
6
- if (require('./sdk-disabled').sdkDisabled('tracing')) return;
9
+ if (require('./sdk-disabled').sdkDisabled('tracing')) { module.exports = NOT_STARTED; return; }
7
10
 
8
11
  /**
9
12
  * Preload with: node --require securenow/register app.js
@@ -127,7 +130,7 @@ console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s credentials=%s
127
130
  process.pid,
128
131
  JSON.stringify(baseName),
129
132
  JSON.stringify(endpointBase),
130
- resolvedApp.appKey ? 'set' : 'none',
133
+ appConfig.resolveApiKey() ? 'set' : 'missing',
131
134
  appConfig.describeCredentialsSource(),
132
135
  serviceName,
133
136
  serviceInstanceId
@@ -239,7 +242,8 @@ if (loggingEnabled) {
239
242
  const batchLogProcessor = new BatchLogRecordProcessor({ exporter: logExporter });
240
243
  loggerProvider = new LoggerProvider({
241
244
  resource: sharedResource,
242
- processors: [batchLogProcessor],
245
+ // Secret shapes in log lines are redacted before export (log-redaction.js).
246
+ processors: require('./log-redaction').logProcessors(appConfig, batchLogProcessor),
243
247
  });
244
248
 
245
249
  // Auto-patch console.* so every log/warn/error becomes an OTel log record
@@ -348,6 +352,8 @@ const sdk = new NodeSDK({
348
352
  instrumentations: [
349
353
  httpInstrumentation,
350
354
  ...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
355
+ // http.route for Hono, h3, Feathers and Polka (no instrumentation in the OpenTelemetry bundle).
356
+ ...(disabledMap['securenow-framework-routes'] ? [] : [new (require('./framework-routes').SecureNowRouteInstrumentation)()]),
351
357
  ...getNodeAutoInstrumentations({
352
358
  ...frameworkInstrumentationConfig,
353
359
  ...disabledMap,
@@ -404,6 +410,8 @@ const sdk = new NodeSDK({
404
410
  aws: firewallOptions.aws,
405
411
  gcp: firewallOptions.gcp,
406
412
  });
413
+ // Zero-code session revocation / session-theft capture (config.sessions.guard / .capture).
414
+ require('./sessions').installHttpGuard(appConfig.resolveSessionsOptions());
407
415
  }
408
416
  } catch (e) {
409
417
  console.error('[securenow] OTel start failed:', e && e.stack || e);