securenow 8.24.0 → 8.25.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.
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Exported log lines are redacted before they leave the process (the terminal still shows them as
5
+ * written). Two passes:
6
+ * - Credential fields, by exact name (case and separators ignored): in objects passed to console.*
7
+ * (redactLogArgs, before formatting) and in JSON or key=value text inside the line. password,
8
+ * token, apiKey, authorization, cookie, OTP codes, card numbers... `code` only when it holds a
9
+ * 4-8 digit OTP, so error codes stay readable.
10
+ * - Secret shapes anywhere in the line, as for captured request bodies: JWTs, Bearer/Basic
11
+ * credentials, sk_/pk_/rk_/snk_ keys, card numbers (Luhn-checked), emails.
12
+ * The second pass runs as the first log processor, so console.*, getLogger().emit() and crash logs
13
+ * are all covered. Attributes set on getLogger().emit() are exported as set.
14
+ *
15
+ * config.logging.redact: false exports lines unchanged.
16
+ */
17
+
18
+ // Lines longer than this skip the shape scan (the credential and OTP assignment scans still run).
19
+ const MAX_LOG_SCAN_LENGTH = 262144;
20
+
21
+ const CREDENTIAL_KEYS = new Set([
22
+ 'password', 'passwd', 'pwd', 'passphrase', 'secret', 'clientsecret', 'privatekey',
23
+ 'token', 'accesstoken', 'refreshtoken', 'idtoken', 'sessiontoken', 'authtoken', 'bearer',
24
+ 'apikey', 'xapikey', 'authorization', 'cookie', 'setcookie', 'sessionid',
25
+ 'otp', 'otpcode', 'onetimecode', 'onetimepassword', 'passcode', 'verificationcode',
26
+ 'confirmationcode', 'mfacode', 'totpcode', '2facode', 'smscode', 'emailcode', 'logincode',
27
+ 'securitycode', 'recoverycode', 'backupcode',
28
+ 'cardnumber', 'cvv', 'cvc', 'ccv', 'ssn', 'pin',
29
+ ]);
30
+ const canonical = (key) => String(key).replace(/[^a-z0-9]/gi, '').toLowerCase();
31
+ const OTP_DIGITS = /^\d{4,8}$/;
32
+
33
+ function isCredentialKey(key, value) {
34
+ const name = canonical(key);
35
+ if (CREDENTIAL_KEYS.has(name)) return true;
36
+ return name === 'code' && OTP_DIGITS.test(String(value));
37
+ }
38
+
39
+ let _enabled;
40
+ function redactionEnabled() {
41
+ if (_enabled === undefined) {
42
+ try { _enabled = require('./app-config').boolConfig('logging.redact', true); } catch (_) { _enabled = true; }
43
+ }
44
+ return _enabled;
45
+ }
46
+
47
+ const MAX_DEPTH = 8;
48
+ const MAX_NODES = 2000;
49
+
50
+ function isPlainObject(v) {
51
+ if (!v || typeof v !== 'object') return false;
52
+ const proto = Object.getPrototypeOf(v);
53
+ return proto === Object.prototype || proto === null;
54
+ }
55
+
56
+ // Copies plain objects and arrays with credential values replaced. Class instances (Date, Map,
57
+ // Buffer, Error...) are kept as they are; cycles stay cycles; work is bounded.
58
+ function redactValue(value, state, depth) {
59
+ if (!(isPlainObject(value) || Array.isArray(value))) return value;
60
+ if (state.seen.has(value)) return state.seen.get(value);
61
+ if (depth > MAX_DEPTH || state.nodes > MAX_NODES) return value;
62
+ state.nodes += 1;
63
+ const out = Array.isArray(value) ? [] : {};
64
+ state.seen.set(value, out);
65
+ for (const key of Object.keys(value)) {
66
+ const v = value[key];
67
+ out[key] = !Array.isArray(value) && isCredentialKey(key, v) && v !== null && v !== undefined && v !== ''
68
+ ? '[REDACTED]'
69
+ : redactValue(v, state, depth + 1);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** console.* arguments with credential fields redacted in objects (unchanged when redaction is off). */
75
+ function redactLogArgs(args) {
76
+ if (!Array.isArray(args) || !redactionEnabled()) return args;
77
+ let changed = false;
78
+ const state = { seen: new Map(), nodes: 0 };
79
+ const out = args.map((a) => {
80
+ const r = redactValue(a, state, 0);
81
+ if (r !== a) changed = true;
82
+ return r;
83
+ });
84
+ return changed ? out : args;
85
+ }
86
+
87
+ // "password": "x" (JSON) and password=x (key=value) in the text. Starts only at a quote or at the
88
+ // beginning of a word, so matching stays linear.
89
+ const JSON_FIELD = /"([A-Za-z0-9_-]{1,40})"(\s*:\s*)"((?:[^"\\]|\\.)*)"/g;
90
+ const KV_FIELD = /(^|[\s,;&?{(])([A-Za-z0-9_-]{1,40})=([^\s&,;)}]+)/g;
91
+
92
+ function redactCredentialText(text) {
93
+ if (text.indexOf('"') !== -1) {
94
+ text = text.replace(JSON_FIELD, (m, key, sep, value) => (isCredentialKey(key, value) && value ? `"${key}"${sep}"[REDACTED]"` : m));
95
+ }
96
+ if (text.indexOf('=') !== -1) {
97
+ text = text.replace(KV_FIELD, (m, lead, key, value) => (isCredentialKey(key, value) ? `${lead}${key}=[REDACTED]` : m));
98
+ }
99
+ return text;
100
+ }
101
+
102
+ function redactLogText(body) {
103
+ const { redactSensitiveValue } = require('./request-capture');
104
+ return redactSensitiveValue(redactCredentialText(body), MAX_LOG_SCAN_LENGTH);
105
+ }
106
+
107
+ function createRedactingLogProcessor() {
108
+ return {
109
+ onEmit(record) {
110
+ try {
111
+ const body = record && record.body;
112
+ if (typeof body !== 'string' || !body) return;
113
+ const out = redactLogText(body);
114
+ if (out !== body) record.setBody(out);
115
+ } catch (_) { /* never break logging */ }
116
+ },
117
+ forceFlush() { return Promise.resolve(); },
118
+ shutdown() { return Promise.resolve(); },
119
+ };
120
+ }
121
+
122
+ // The processors a LoggerProvider gets: the redactor (unless config.logging.redact is false),
123
+ // then the exporting processor.
124
+ function logProcessors(appConfig, exportingProcessor) {
125
+ const redact = appConfig.boolConfig('logging.redact', true);
126
+ return redact ? [createRedactingLogProcessor(), exportingProcessor] : [exportingProcessor];
127
+ }
128
+
129
+ module.exports = {
130
+ createRedactingLogProcessor,
131
+ logProcessors,
132
+ redactLogArgs,
133
+ redactLogText,
134
+ MAX_LOG_SCAN_LENGTH,
135
+ };
package/mcp/catalog.js CHANGED
@@ -46,21 +46,26 @@ Runbook:
46
46
  - Confirm the runtime file has an app key/name/instance and a runtime API key after app selection. The app key only routes telemetry; the runtime API key is required to authenticate telemetry ingestion and firewall sync.
47
47
  - Confirm .securenow/admin.json exists only when admin CLI/MCP auth is needed.
48
48
  - Confirm .securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
49
- 6. Run npx securenow init. If it fails with ui.header is not a function or another CLI bug, upgrade to securenow@latest, verify >=8.0.0, and retry. Do not silently ignore init failures.
50
- 7. Configure the least invasive framework-specific integration:
51
- - 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.
52
- - Nuxt/Nitro: use the documented securenow/nuxt module or Nitro server plugin.
53
- - Express/Fastify/NestJS/Koa/Hapi/Hono/raw Node: preload securenow/register through existing scripts, NODE_OPTIONS, PM2 node_args, Docker CMD, or the process manager already used.
54
- - Firewall-only: preload securenow/firewall-only or use the documented securenow run --firewall-only command. Do not add OTel/tracing/logging in this mode.
55
- - Vite/browser-only: use only documented browser integration and state that server firewall protection requires a server runtime.
49
+ 6. Run npx securenow init. It detects the framework, writes the Next.js instrumentation file and next.config changes, prints the start-script change for plain Node, gitignores the credential files, and warns about a PM2 ecosystem file that uses a bare -r. If it fails, upgrade to securenow@latest and retry. Do not silently ignore init failures.
50
+ 7. Configure the least invasive framework-specific integration (help: npx securenow help init; full guide: node_modules/securenow/SKILL-API.md):
51
+ - Plain Node servers (Express, Fastify, NestJS, Koa, Hapi, Hono with @hono/node-server, Polka, Feathers, h3, raw node:http): preload securenow/register in the existing start command: node -r securenow/register <entry>. The same flag works for CommonJS, ESM and TypeScript (node -r securenow/register --import tsx src/index.ts). Alternatives: NODE_OPTIONS="-r securenow/register", npx securenow run <entry>, or require('securenow/register') as the first line of a CommonJS entry.
52
+ - PM2: node_args: ['-r', require.resolve('securenow/register')] in ecosystem.config.cjs (fork or cluster). Never a bare '-r securenow/register' in PM2: cluster workers resolve it from the daemon's folder and crash-loop.
53
+ - Docker: CMD ["node", "-r", "securenow/register", "<entry>"] with WORKDIR set to the app folder.
54
+ - Next.js 15 and 16: keep the instrumentation file npx securenow init writes (it imports securenow/nextjs and securenow/nextjs-auto-capture inside if (process.env.NEXT_RUNTIME === 'nodejs') with a try/catch) and its next.config changes: serverExternalPackages: ['securenow'] plus outputFileTracingIncludes: { '/*': ['./node_modules/securenow/**/*'] }. Next.js 14: experimental.instrumentationHook, experimental.serverComponentsExternalPackages and experimental.outputFileTracingIncludes instead. Works with next start and output: 'standalone' (put .securenow/credentials.json next to the standalone server.js). Preserve existing instrumentation, proxy.js and middleware.js code. The Edge runtime is not instrumented.
55
+ - Nuxt 3/4: modules: ['securenow/nuxt'] in nuxt.config, no securenow options block (settings live in .securenow/runtime.json). Alternative: node -r securenow/register .output/server/index.mjs.
56
+ - Vite: browser telemetry is not sent to SecureNow. Instrument the Node process that serves the app: node -r securenow/register-vite node_modules/vite/bin/vite.js preview, or -r securenow/register for a Vite SSR server. Say that the firewall only protects a server runtime.
57
+ - Bun 1.3: bun --preload securenow/register <entry> for apps served through node:http. Bun.serve, Deno, Cloudflare Workers and other edge runtimes are not supported.
58
+ - Firewall-only: node -r securenow/firewall-only <entry>, or npx securenow run --firewall-only <entry>. Do not add OTel, tracing or logging in this mode.
59
+ - App-level features, when the app has authentication: send auth events with track() from securenow/events (auth.login.failure, auth.login.success, auth.otp.verify.failure, auth.password_reset.requested, ...) at the real login/OTP/reset code paths, and enable session revocation with "sessions": { "guard": true, "capture": true } under config in .securenow/runtime.json (add the app's session cookie names to sessions.cookieNames if they are not the defaults). Ask before changing auth code.
56
60
  8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/runtime.json, with legacy .securenow/credentials.json and generated runtime credential files still supported:
57
61
  - config.logging.enabled: true
58
62
  - config.capture.body: true
59
63
  - config.capture.multipart: true
60
- - config.firewall.enabled: true
61
64
  - config.firewall.failMode: "open"
62
65
  - config.capture.maxBodySize: 10240
63
- For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Do not recommend env vars unless the user explicitly asks for legacy fallbacks.
66
+ - config.logging.redact: true (credential fields and secret shapes in exported log lines become [REDACTED])
67
+ Firewall enforcement is the per-app toggle in SecureNow (npx securenow firewall enable --app <appKey>), not a local setting.
68
+ For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Hosts that cannot mount a file (Vercel, Railway, Render, Heroku, Fly): npx securenow credentials runtime --env production --format env prints SECURENOW_CREDENTIALS=<base64>; store it as a secret environment variable. No other SECURENOW_* variable is read (SECURENOW_DISABLED=1 turns the SDK off, for tests). Vercel, AWS Lambda and other serverless platforms are not tested end to end: say so, and do not promise firewall blocking there.
64
69
  Do not proceed with telemetry verification if the runtime API key is missing; run npx securenow app connect or npx securenow api-key create first. The v8 ingestion gateway rejects app-key-only telemetry.
65
70
  Local credentials should use config.runtime.deploymentEnvironment="local". Production runtime credentials should use "production". The app key stays the same; traces, logs, firewall status, forensics, and CLI/MCP queries are scoped by environment.
66
71
  9. Verify firewall and threshold:
package/next-route.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * http.route on the HTTP-layer span of a Next.js request.
5
+ *
6
+ * Next.js records the route on its own spans (next.route, often several levels below the request's HTTP
7
+ * span); detection and the dashboard read the HTTP span, which had none on Next.js 15 (16 sets http.route
8
+ * on its own span only). This span processor copies the first route Next.js reports in a trace onto that
9
+ * trace's HTTP server span, which is still open because Next.js finishes its spans before the response ends.
10
+ */
11
+
12
+ const HTTP_SCOPE = '@opentelemetry/instrumentation-http';
13
+ const SERVER = 1; // SpanKind.SERVER
14
+ const MAX_OPEN = 10000;
15
+
16
+ function scopeName(span) {
17
+ const scope = span && (span.instrumentationScope || span.instrumentationLibrary);
18
+ return scope && scope.name;
19
+ }
20
+
21
+ function createNextRouteProcessor() {
22
+ const open = new Map(); // traceId -> { span, routed }
23
+
24
+ return {
25
+ onStart(span) {
26
+ try {
27
+ if (span.kind !== SERVER || scopeName(span) !== HTTP_SCOPE) return;
28
+ const { traceId } = span.spanContext();
29
+ if (open.has(traceId)) return;
30
+ if (open.size >= MAX_OPEN) open.clear();
31
+ open.set(traceId, { span, spanId: span.spanContext().spanId, routed: false });
32
+ } catch (_) {}
33
+ },
34
+ onEnd(span) {
35
+ try {
36
+ const { traceId, spanId } = span.spanContext();
37
+ const entry = open.get(traceId);
38
+ if (!entry) return;
39
+ if (entry.spanId === spanId) {
40
+ open.delete(traceId);
41
+ return;
42
+ }
43
+ const route = span.attributes && span.attributes['next.route'];
44
+ // Next.js also reports module paths here ("\hello\route" on Windows): only URL routes count.
45
+ if (!entry.routed && typeof route === 'string' && route.startsWith('/')) {
46
+ entry.span.setAttribute('http.route', route);
47
+ entry.routed = true;
48
+ }
49
+ } catch (_) {}
50
+ },
51
+ forceFlush() {
52
+ return Promise.resolve();
53
+ },
54
+ shutdown() {
55
+ open.clear();
56
+ return Promise.resolve();
57
+ },
58
+ };
59
+ }
60
+
61
+ module.exports = { createNextRouteProcessor };
@@ -51,18 +51,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
51
51
  // and they only ever transform captured telemetry strings (read-only) — never
52
52
  // the actual request/response stream. Compiled once at module load.
53
53
  const VALUE_REDACTORS = [
54
+ // The JWT and email patterns only start where a run of their characters starts (the
55
+ // lookbehind), so a long run that holds no match is scanned once, not once per position:
56
+ // a crafted 10KB value costs microseconds, not 50ms+ of event loop.
54
57
  // JWT: three base64url segments. Anchored to the eyJ header so it won't
55
58
  // match arbitrary dotted tokens.
56
- { name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
59
+ { 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 },
57
60
  // Bearer/Basic auth header value embedded in a body string.
58
61
  { name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
59
62
  // Stripe-style / SecureNow live+test API keys.
60
63
  { name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
61
- // Email addresses (bounded local/domain parts).
62
- { name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
64
+ // Email addresses (bounded local/domain parts: 64 and 253 characters at most).
65
+ { name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
63
66
  // Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
64
67
  // space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
65
- { name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
68
+ { name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
66
69
  ];
67
70
 
68
71
  // Skip the value-shape scan on very large strings to bound per-body cost.
@@ -93,6 +96,7 @@ function redactSensitiveValue(value) {
93
96
  if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
94
97
  let out = value;
95
98
  for (const r of VALUE_REDACTORS) {
99
+ if (r.hint && !out.includes(r.hint)) continue;
96
100
  r.re.lastIndex = 0;
97
101
  if (r.luhn) {
98
102
  out = out.replace(r.re, (m) => {
package/nextjs-wrapper.js CHANGED
@@ -51,18 +51,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
51
51
  // and they only ever transform captured telemetry strings (read-only) — never
52
52
  // the actual request/response stream. Compiled once at module load.
53
53
  const VALUE_REDACTORS = [
54
+ // The JWT and email patterns only start where a run of their characters starts (the
55
+ // lookbehind), so a long run that holds no match is scanned once, not once per position:
56
+ // a crafted 10KB value costs microseconds, not 50ms+ of event loop.
54
57
  // JWT: three base64url segments. Anchored to the eyJ header so it won't
55
58
  // match arbitrary dotted tokens.
56
- { name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
59
+ { 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 },
57
60
  // Bearer/Basic auth header value embedded in a body string.
58
61
  { name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
59
62
  // Stripe-style / SecureNow live+test API keys.
60
63
  { name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
61
- // Email addresses (bounded local/domain parts).
62
- { name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
64
+ // Email addresses (bounded local/domain parts: 64 and 253 characters at most).
65
+ { name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
63
66
  // Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
64
67
  // space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
65
- { name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
68
+ { name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
66
69
  ];
67
70
 
68
71
  // Skip the value-shape scan on very large strings to bound per-body cost.
@@ -93,6 +96,7 @@ function redactSensitiveValue(value) {
93
96
  if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
94
97
  let out = value;
95
98
  for (const r of VALUE_REDACTORS) {
99
+ if (r.hint && !out.includes(r.hint)) continue;
96
100
  r.re.lastIndex = 0;
97
101
  if (r.luhn) {
98
102
  out = out.replace(r.re, (m) => {
package/nextjs.d.ts CHANGED
@@ -1,102 +1,70 @@
1
1
  /**
2
2
  * SecureNow Next.js Integration TypeScript Declarations
3
+ *
4
+ * Settings live in .securenow/runtime.json (or SECURENOW_CREDENTIALS on hosts without file
5
+ * mounts); the options below only override them.
3
6
  */
4
7
 
5
8
  export interface RegisterOptions {
6
9
  /**
7
- * Service name for OpenTelemetry traces
8
- * @default .securenow/credentials.json app.key/app.name
10
+ * Service name for OpenTelemetry traces.
11
+ * Leave unset: SecureNow routes telemetry by the app key, and another name hides the data
12
+ * from the app's dashboard. Only for a self-hosted collector.
13
+ * @default the app key from .securenow/runtime.json
9
14
  */
10
15
  serviceName?: string;
11
16
 
12
17
  /**
13
- * OTLP endpoint for traces
18
+ * Advanced OTLP endpoint override, for a self-hosted collector. Leave unset for SecureNow.
14
19
  * @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.
19
20
  */
20
21
  endpoint?: string;
21
22
 
22
23
  /**
23
24
  * Deployment environment sent as OpenTelemetry deployment.environment.
24
- * @default .securenow/credentials.json config.runtime.deploymentEnvironment
25
+ * @default config.runtime.deploymentEnvironment from .securenow/runtime.json
25
26
  */
26
27
  environment?: string;
27
28
 
28
29
  /**
29
- * Don't append UUID to service name
30
- * @default false
30
+ * Don't append a UUID to the service name.
31
+ * @default true when an app key is configured
31
32
  */
32
33
  noUuid?: boolean;
33
34
 
34
- /**
35
- * Enable request body capture
36
- * @default true from .securenow/credentials.json secure defaults
35
+ /**
36
+ * Capture request bodies (redacted, size-capped, multipart as metadata only).
37
+ * @default config.capture.body from .securenow/runtime.json (true)
37
38
  */
38
39
  captureBody?: boolean;
39
40
  }
40
41
 
41
42
  /**
42
- * Register SecureNow OpenTelemetry instrumentation for Next.js
43
- *
44
- * @param options - Optional configuration options
45
- *
46
- * @example
47
- * ```typescript
48
- * // instrumentation.ts
49
- * import { createRequire } from 'node:module';
50
- *
51
- * const require = createRequire(import.meta.url);
43
+ * Register SecureNow (tracing, logs, body capture, firewall, sessions) for the Next.js Node.js runtime.
44
+ * `npx securenow init` writes this file for you.
45
+ *
46
+ * @param options - Optional overrides
52
47
  *
53
- * export async function register() {
54
- * if (process.env.NEXT_RUNTIME !== 'nodejs') return;
55
- * const { registerSecureNow } = require('securenow/nextjs');
56
- * registerSecureNow({ captureBody: true });
57
- * require('securenow/nextjs-auto-capture');
58
- * }
59
- * ```
60
- *
61
48
  * @example
62
49
  * ```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
- * });
50
+ * // instrumentation.ts (what `npx securenow init` writes)
51
+ * export async function register() {
52
+ * if (process.env.NEXT_RUNTIME === 'nodejs') {
53
+ * try {
54
+ * const securenowNext = await import('securenow/nextjs');
55
+ * const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
56
+ * registerSecureNow({ captureBody: true });
57
+ * await import('securenow/nextjs-auto-capture');
58
+ * } catch (error) {
59
+ * console.error('[securenow] SecureNow did not start; the app is running without it:', error);
60
+ * }
61
+ * }
72
62
  * }
73
63
  * ```
64
+ *
65
+ * next.config: `serverExternalPackages: ['securenow']` and
66
+ * `outputFileTracingIncludes: { '/*': ['./node_modules/securenow/**\/*'] }` (Next.js 15+);
67
+ * Next.js 14 uses `experimental.instrumentationHook`, `experimental.serverComponentsExternalPackages`
68
+ * and `experimental.outputFileTracingIncludes`.
74
69
  */
75
70
  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;
package/nextjs.js CHANGED
@@ -140,7 +140,8 @@ function registerSecureNow(options = {}) {
140
140
  const otelLogLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
141
141
  const isDevelopmentRuntime = process.env.NODE_ENV === 'development';
142
142
 
143
- console.log('[securenow] Next.js App -> service.name=%s', serviceName);
143
+ console.log('[securenow] Next.js App -> service.name=%s apiKey=%s credentials=%s', serviceName,
144
+ appConfig.resolveApiKey() ? 'set' : 'missing', appConfig.describeCredentialsSource());
144
145
  console.log('[securenow] Environment: %s', deploymentEnvironment);
145
146
 
146
147
  // -------- Body Capture Configuration --------
@@ -438,10 +439,16 @@ function registerSecureNow(options = {}) {
438
439
  headers
439
440
  }));
440
441
 
442
+ // The batch exporter NodeSDK would build from traceExporter, plus the processor that puts Next.js's
443
+ // route on the request's HTTP span (see next-route.js).
444
+ const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
441
445
  const sdk = new NodeSDK({
442
446
  ...nodeSdkTelemetryOptions,
443
447
  serviceName: serviceName,
444
- traceExporter: traceExporter,
448
+ spanProcessors: [
449
+ new BatchSpanProcessor(traceExporter),
450
+ require('./next-route').createNextRouteProcessor(),
451
+ ],
445
452
  instrumentations: [httpInstrumentation],
446
453
  resource: createResource({
447
454
  [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
@@ -472,16 +479,21 @@ function registerSecureNow(options = {}) {
472
479
  [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
473
480
  }),
474
481
  // sdk-logs >= 0.220 takes a single options object ({ exporter }).
475
- processors: [new BatchLogRecordProcessor({ exporter: logExporter })],
482
+ // Secret shapes in log lines are redacted before export (log-redaction.js).
483
+ processors: require('./log-redaction').logProcessors(appConfig, new BatchLogRecordProcessor({ exporter: logExporter })),
476
484
  });
477
485
  _crashLoggerProvider = loggerProvider;
478
486
 
479
- // Patch console to forward logs as OTLP log records
487
+ // Patch console to forward logs as OTLP log records, formatted like the Node preload's
488
+ // (printf placeholders, JSON objects, errors with their stack).
480
489
  const logger = loggerProvider.getLogger('console', '1.0.0');
481
- const SeverityNumber = { INFO: 9, WARN: 13, ERROR: 17 };
490
+ const SeverityNumber = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
482
491
  const origLog = console.log;
492
+ const origInfo = console.info;
483
493
  const origWarn = console.warn;
484
494
  const origError = console.error;
495
+ const origDebug = console.debug;
496
+ const { formatLogArgs } = require('./log-format');
485
497
 
486
498
  const { context: otelContext, trace: otelTrace } = require('@opentelemetry/api');
487
499
  function _emitLog(sn, st, args) {
@@ -491,7 +503,8 @@ function registerSecureNow(options = {}) {
491
503
  logger.emit({
492
504
  severityNumber: sn,
493
505
  severityText: st,
494
- body: args.map(String).join(' '),
506
+ body: formatLogArgs(args),
507
+ attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
495
508
  ...(spanCtx && { context: activeCtx }),
496
509
  });
497
510
  } catch (_) {}
@@ -500,6 +513,10 @@ function registerSecureNow(options = {}) {
500
513
  origLog.apply(console, args);
501
514
  _emitLog(SeverityNumber.INFO, 'INFO', args);
502
515
  };
516
+ console.info = (...args) => {
517
+ origInfo.apply(console, args);
518
+ _emitLog(SeverityNumber.INFO, 'INFO', args);
519
+ };
503
520
  console.warn = (...args) => {
504
521
  origWarn.apply(console, args);
505
522
  _emitLog(SeverityNumber.WARN, 'WARN', args);
@@ -508,6 +525,10 @@ function registerSecureNow(options = {}) {
508
525
  origError.apply(console, args);
509
526
  _emitLog(SeverityNumber.ERROR, 'ERROR', args);
510
527
  };
528
+ console.debug = (...args) => {
529
+ origDebug.apply(console, args);
530
+ _emitLog(SeverityNumber.DEBUG, 'DEBUG', args);
531
+ };
511
532
 
512
533
  console.log('[securenow] Logging: ENABLED -> %s', logsUrl);
513
534
 
@@ -627,6 +648,8 @@ function registerSecureNow(options = {}) {
627
648
  aws: firewallOptions.aws,
628
649
  gcp: firewallOptions.gcp,
629
650
  });
651
+ // Zero-code session revocation / session-theft capture (config.sessions.guard / .capture).
652
+ requireRuntimeModule('./sessions').installHttpGuard(appConfig.resolveSessionsOptions());
630
653
  } catch (e) {
631
654
  console.warn('[securenow] Firewall init failed:', e.message);
632
655
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "8.24.0",
3
+ "version": "8.25.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-firewall-guards.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-crash-capture.js && node test-crash-flush.js && node test-request-capture.js && node test-fastify-hooks.js && node test-pm2-credentials.js && node test-env-credentials.js && node test-framework-wiring.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-esm-detect.js && node test-package-contents.js"
14
+ "test": "node test-app-config.js && node test-resolve-ip.js && node test-cidr.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-firewall-guards.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-log-redaction.js && node test-crash-capture.js && node test-crash-flush.js && node test-request-capture.js && node test-fastify-hooks.js && node test-pm2-credentials.js && node test-env-credentials.js && node test-framework-wiring.js && node test-framework-routes.js && node test-sessions-everywhere.js && node test-local-socket.js && node test-firewall-cloud.js && node test-firewall-layers.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-esm-detect.js && node test-package-contents.js"
15
15
  },
16
16
  "keywords": [
17
17
  "opentelemetry",
@@ -167,10 +167,13 @@
167
167
  "firewall-paths.js",
168
168
  "rate-limit-store.js",
169
169
  "log-format.js",
170
+ "log-redaction.js",
170
171
  "crash-capture.js",
171
172
  "request-capture.js",
172
173
  "pm2-env.js",
173
174
  "fastify-hooks.js",
175
+ "framework-routes.js",
176
+ "next-route.js",
174
177
  "sdk-disabled.js",
175
178
  "rate-limits.js",
176
179
  "rate-limits.d.ts",