securenow 8.11.1 → 8.11.2

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 CHANGED
@@ -1307,7 +1307,7 @@ all runtime settings from this JSON plus built-in defaults. Production should
1307
1307
  mount a tokenless runtime credentials file at `.securenow/credentials.json`.
1308
1308
  Environment-variable fallback is not supported.
1309
1309
 
1310
- **Default sensitive fields (auto-redacted):** `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `access_token`, `auth`, `credentials`, `mysql_pwd`, `stripeToken`, `card`, `cardnumber`, `ccv`, `cvc`, `cvv`, `ssn`, `pin`
1310
+ **Default sensitive fields (auto-redacted):** `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `access_token`, `auth`, `credentials`, `mysql_pwd`, `stripeToken`, `card`, `cardnumber`, `ccv`, `cvc`, `cvv`, `ssn`, `pin`, `code`, `otpCode`, `oneTimeCode`, `verificationCode`, `passcode`
1311
1311
 
1312
1312
  For instrumentation, firewall layers, debugging, trusted proxies, and deployment
1313
1313
  environment, edit the matching `config.*` keys in `.securenow/runtime.json`
@@ -1450,6 +1450,8 @@ File binary content is never stored in traces.
1450
1450
 
1451
1451
  ### Sensitive Data Redaction
1452
1452
 
1453
+ SecureNow 8.11.2 redacts sign-in credentials named `code`, `otpCode`, `oneTimeCode`, `verificationCode`, and `passcode` by default, including case and separator variants. JSON, form, GraphQL, and multipart text capture use the same rule; URL query parameters and captured headers are also protected. Credential names match exactly after normalization, so `status_code` and `error_code` remain available for investigation. Redaction changes exported telemetry only.
1454
+
1453
1455
  All request bodies are automatically scanned and sensitive fields are redacted:
1454
1456
 
1455
1457
  **Automatically redacted fields:** `password`, `secret`, `token`, `api_key`, `card`, `cvv`, `ssn`, and more.
package/README.md CHANGED
@@ -228,6 +228,8 @@ It syncs your app's blocklist plus SecureNow's IP database of 500k+ known-bad IP
228
228
 
229
229
  - **SecureNow never needs your source code.** It sees runtime telemetry — requests, spans, logs — not your repository.
230
230
  - **Redacted at capture.** `password`, `token`, `api_key`, `authorization`, `cookie`, and friends are auto-redacted before leaving your process; multipart file content is never captured. Preview with `securenow redact '<json>'`.
231
+
232
+ SecureNow 8.11.2 redacts sign-in credentials named `code`, `otpCode`, `oneTimeCode`, `verificationCode`, and `passcode` by default, including case and separator variants. JSON, form, GraphQL, and multipart text capture use the same rule; URL query parameters and captured headers are also protected. Credential names match exactly after normalization, so `status_code` and `error_code` remain available for investigation. Redaction changes exported telemetry only.
231
233
  - **Fails open.** If SecureNow is unreachable, your app serves traffic normally (`config.firewall.failMode: "open"` is the default).
232
234
  - **Read/write split.** MCP read tools are read-only; write tools require explicit `confirm: true` plus a reason, and human approval is the designed path for mutations — blocks, rate limits, and revocations are proposed, approved, executed, and reversible.
233
235
 
package/SKILL-API.md CHANGED
@@ -542,6 +542,8 @@ securenow cidr parse 10.0.0.0/8 # network/broadcast/
542
542
 
543
543
  ## Sensitive Data Redaction
544
544
 
545
+ SecureNow 8.11.2 redacts sign-in credentials named `code`, `otpCode`, `oneTimeCode`, `verificationCode`, and `passcode` by default, including case and separator variants. JSON, form, GraphQL, and multipart text capture use the same rule; URL query parameters and captured headers are also protected. Credential names match exactly after normalization, so `status_code` and `error_code` remain available for investigation. Redaction changes exported telemetry only.
546
+
545
547
  Available from multiple entry points (`securenow/nextjs-middleware`, `securenow/nextjs-wrapper`, `securenow/nextjs-auto-capture`):
546
548
 
547
549
  ```javascript
@@ -551,7 +553,7 @@ const safe = redactSensitiveData({ username: 'alice', password: 's3cret', token:
551
553
  // { username: 'alice', password: '[REDACTED]', token: '[REDACTED]' }
552
554
  ```
553
555
 
554
- **Auto-redacted fields:** `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `access_token`, `auth`, `credentials`, `mysql_pwd`, `stripeToken`, `card`, `cardnumber`, `ccv`, `cvc`, `cvv`, `ssn`, `pin`.
556
+ **Auto-redacted fields:** `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `access_token`, `auth`, `credentials`, `mysql_pwd`, `stripeToken`, `card`, `cardnumber`, `ccv`, `cvc`, `cvv`, `ssn`, `pin`, `code`, `otpCode`, `oneTimeCode`, `verificationCode`, `passcode`.
555
557
 
556
558
  Add custom fields via `config.capture.sensitiveFields` in `.securenow/runtime.json`.
557
559
 
package/SKILL-CLI.md CHANGED
@@ -538,7 +538,9 @@ securenow test-span "ci.smoke-test" # custom span name
538
538
 
539
539
  Both commands use the default SecureNow ingestion gateway plus optional advanced `config.otel.*` overrides from `.securenow/runtime.json` or legacy runtime credentials, and return non-zero on HTTP errors so CI/cron can detect failures.
540
540
 
541
- ### Utilities — Redaction, CIDR, Diagnostics
541
+ ### Utilities — Redaction, CIDR, Diagnostics
542
+
543
+ The `redact` command also hides OTP credentials named `code`, `oneTimeCode`, `verificationCode`, and `passcode` (case/separator variants included), while preserving `status_code` and `error_code`. SDK capture uses the same defaults.
542
544
 
543
545
  These mirror the SDK exports (`redactSensitiveData`, `createMatcher`, `parseCidr`) so agents can validate behavior without writing Node.
544
546
 
package/cli.js CHANGED
@@ -671,7 +671,7 @@ const COMMANDS = {
671
671
  run: (rawArgs) => require('./cli/run').run(rawArgs),
672
672
  },
673
673
  redact: {
674
- desc: 'Redact sensitive fields from a JSON payload',
674
+ desc: 'Redact sensitive fields and OTP codes from a JSON payload',
675
675
  usage: "securenow redact '<json>' [--fields f1,f2] [--json]",
676
676
  flags: { fields: 'Comma-separated extra field names to redact (added to defaults)' },
677
677
  run: (a, f) => require('./cli/utils').redact(a, f),
package/legacy-semconv.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { isOtpCodeField } = require('./otp-redaction');
4
+
3
5
  /**
4
6
  * Legacy semantic-convention attributes for the OpenTelemetry 0.221+ /
5
7
  * contrib 0.74+ instrumentations, which emit ONLY the stable conventions.
@@ -24,7 +26,7 @@
24
26
  * - http.client_ip is NOT derived from the first X-Forwarded-For entry (that
25
27
  * value is spoofable). SecureNow's requestHook sets http.client_ip from the
26
28
  * trusted-proxy resolver (resolve-ip.js).
27
- * - Signature-style query parameters (same list upstream now redacts on
29
+ * - OTP/code and signature-style query parameters (same list upstream redacts on
28
30
  * url.query / url.full) are replaced with REDACTED in http.target / http.url.
29
31
  * Every other byte of the target is kept exactly as received, so detection
30
32
  * rules that match encoded payloads keep working.
@@ -62,7 +64,6 @@ function defined(attributes) {
62
64
  function redactSearch(search, paramsToRedact = DEFAULT_REDACTED_QUERY_PARAMS) {
63
65
  if (typeof search !== 'string' || search.length < 2 || search[0] !== '?') return search;
64
66
  const sensitive = new Set(paramsToRedact || []);
65
- if (sensitive.size === 0) return search;
66
67
  let changed = false;
67
68
  const parts = search.slice(1).split('&').map((part) => {
68
69
  const eq = part.indexOf('=');
@@ -71,7 +72,7 @@ function redactSearch(search, paramsToRedact = DEFAULT_REDACTED_QUERY_PARAMS) {
71
72
  try {
72
73
  key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
73
74
  } catch (_) {}
74
- if (!sensitive.has(key)) return part;
75
+ if (!sensitive.has(key) && !isOtpCodeField(key)) return part;
75
76
  changed = true;
76
77
  return `${rawKey}=${REDACTED}`;
77
78
  });
@@ -19,14 +19,18 @@
19
19
  const { trace } = require('@opentelemetry/api');
20
20
  const appConfig = require('./app-config');
21
21
 
22
+ const { OTP_SENSITIVE_FIELDS, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery } = require('./otp-redaction');
23
+
22
24
  // Default sensitive fields to redact from request bodies.
23
- // Matched substring-wise against lowercased keys (see redactSensitiveData),
25
+ // OTP/code aliases use exact normalized matching; other fields are matched
26
+ // substring-wise against lowercased keys (see redactSensitiveData),
24
27
  // so e.g. 'card' also catches 'creditCard' and 'account' also catches
25
28
  // 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
26
29
  // telemetry value is always safer than a leaked secret. Entries are kept
27
30
  // specific enough to avoid nuking broad benign keys (e.g. we use
28
31
  // 'firstname'/'lastname'/'fullname', never bare 'name').
29
- const DEFAULT_SENSITIVE_FIELDS = [
32
+ const DEFAULT_SENSITIVE_FIELDS = [
33
+ ...OTP_SENSITIVE_FIELDS,
30
34
  // credentials / auth
31
35
  'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
32
36
  'access_token', 'auth', 'authorization', 'bearer', 'credentials',
@@ -84,7 +88,10 @@ function luhnValid(digits) {
84
88
  */
85
89
  function redactSensitiveValue(value) {
86
90
  if (typeof value !== 'string' || value.length === 0) return value;
87
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
91
+ // Credential assignments in GraphQL/query strings need field-aware
92
+ // redaction even though a short numeric OTP has no unique value shape.
93
+ value = redactOtpQuery(redactOtpGraphQL(value));
94
+ if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
88
95
  let out = value;
89
96
  for (const r of VALUE_REDACTORS) {
90
97
  r.re.lastIndex = 0;
@@ -110,9 +117,8 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
110
117
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
111
118
 
112
119
  for (const key of Object.keys(redacted)) {
113
- const lowerKey = key.toLowerCase();
114
120
 
115
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
121
+ if (matchesSensitiveField(key, sensitiveFields)) {
116
122
  redacted[key] = '[REDACTED]';
117
123
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
118
124
  redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
@@ -18,14 +18,18 @@
18
18
  const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
19
19
  const appConfig = require('./app-config');
20
20
 
21
+ const { OTP_SENSITIVE_FIELDS, isOtpCodeField, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery } = require('./otp-redaction');
22
+
21
23
  // Default sensitive fields to redact from request bodies.
22
- // Matched substring-wise against lowercased keys (see redactSensitiveData),
24
+ // OTP/code aliases use exact normalized matching; other fields are matched
25
+ // substring-wise against lowercased keys (see redactSensitiveData),
23
26
  // so e.g. 'card' also catches 'creditCard' and 'account' also catches
24
27
  // 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
25
28
  // telemetry value is always safer than a leaked secret. Entries are kept
26
29
  // specific enough to avoid nuking broad benign keys (e.g. we use
27
30
  // 'firstname'/'lastname'/'fullname', never bare 'name').
28
- const DEFAULT_SENSITIVE_FIELDS = [
31
+ const DEFAULT_SENSITIVE_FIELDS = [
32
+ ...OTP_SENSITIVE_FIELDS,
29
33
  // credentials / auth
30
34
  'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
31
35
  'access_token', 'auth', 'authorization', 'bearer', 'credentials',
@@ -83,7 +87,10 @@ function luhnValid(digits) {
83
87
  */
84
88
  function redactSensitiveValue(value) {
85
89
  if (typeof value !== 'string' || value.length === 0) return value;
86
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
90
+ // Credential assignments in GraphQL/query strings need field-aware
91
+ // redaction even though a short numeric OTP has no unique value shape.
92
+ value = redactOtpQuery(redactOtpGraphQL(value));
93
+ if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
87
94
  let out = value;
88
95
  for (const r of VALUE_REDACTORS) {
89
96
  r.re.lastIndex = 0;
@@ -113,9 +120,8 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
113
120
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
114
121
 
115
122
  for (const key of Object.keys(redacted)) {
116
- const lowerKey = key.toLowerCase();
117
123
 
118
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
124
+ if (matchesSensitiveField(key, sensitiveFields)) {
119
125
  redacted[key] = '[REDACTED]';
120
126
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
121
127
  redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
@@ -137,7 +143,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
137
143
 
138
144
  let redacted = query;
139
145
 
140
- sensitiveFields.forEach(field => {
146
+ sensitiveFields.filter(field => field === 'otp' || !isOtpCodeField(field)).forEach(field => {
141
147
  const escaped = escapeRegex(field);
142
148
  const patterns = [
143
149
  new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
@@ -153,7 +159,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
153
159
 
154
160
  // Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
155
161
  // body — catches values the key-name pass above can't see.
156
- redacted = redactSensitiveValue(redacted);
162
+ redacted = redactOtpGraphQL(redactSensitiveValue(redacted));
157
163
 
158
164
  return redacted;
159
165
  }
package/nextjs-wrapper.js CHANGED
@@ -18,14 +18,18 @@
18
18
  const { trace } = require('@opentelemetry/api');
19
19
  const appConfig = require('./app-config');
20
20
 
21
+ const { OTP_SENSITIVE_FIELDS, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery } = require('./otp-redaction');
22
+
21
23
  // Default sensitive fields to redact from request bodies.
22
- // Matched substring-wise against lowercased keys (see redactSensitiveData),
24
+ // OTP/code aliases use exact normalized matching; other fields are matched
25
+ // substring-wise against lowercased keys (see redactSensitiveData),
23
26
  // so e.g. 'card' also catches 'creditCard' and 'account' also catches
24
27
  // 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
25
28
  // telemetry value is always safer than a leaked secret. Entries are kept
26
29
  // specific enough to avoid nuking broad benign keys (e.g. we use
27
30
  // 'firstname'/'lastname'/'fullname', never bare 'name').
28
- const DEFAULT_SENSITIVE_FIELDS = [
31
+ const DEFAULT_SENSITIVE_FIELDS = [
32
+ ...OTP_SENSITIVE_FIELDS,
29
33
  // credentials / auth
30
34
  'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
31
35
  'access_token', 'auth', 'authorization', 'bearer', 'credentials',
@@ -83,7 +87,10 @@ function luhnValid(digits) {
83
87
  */
84
88
  function redactSensitiveValue(value) {
85
89
  if (typeof value !== 'string' || value.length === 0) return value;
86
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
90
+ // Credential assignments in GraphQL/query strings need field-aware
91
+ // redaction even though a short numeric OTP has no unique value shape.
92
+ value = redactOtpQuery(redactOtpGraphQL(value));
93
+ if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
87
94
  let out = value;
88
95
  for (const r of VALUE_REDACTORS) {
89
96
  r.re.lastIndex = 0;
@@ -109,9 +116,8 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
109
116
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
110
117
 
111
118
  for (const key of Object.keys(redacted)) {
112
- const lowerKey = key.toLowerCase();
113
119
 
114
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
120
+ if (matchesSensitiveField(key, sensitiveFields)) {
115
121
  redacted[key] = '[REDACTED]';
116
122
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
117
123
  redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
package/nextjs.js CHANGED
@@ -59,14 +59,18 @@ function createResource(attributes) {
59
59
  throw new Error('Unsupported @opentelemetry/resources version');
60
60
  }
61
61
 
62
+ const { OTP_SENSITIVE_FIELDS, isOtpCodeField, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery, withOtpRedactionExporter } = require('./otp-redaction');
63
+
62
64
  // Default sensitive fields to redact from request bodies.
63
- // Matched substring-wise against lowercased keys (see redactSensitiveData),
65
+ // OTP/code aliases use exact normalized matching; other fields are matched
66
+ // substring-wise against lowercased keys (see redactSensitiveData),
64
67
  // so e.g. 'card' also catches 'creditCard' and 'account' also catches
65
68
  // 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
66
69
  // telemetry value is always safer than a leaked secret. Entries are kept
67
70
  // specific enough to avoid nuking broad benign keys (e.g. we use
68
71
  // 'firstname'/'lastname'/'fullname', never bare 'name').
69
72
  const DEFAULT_SENSITIVE_FIELDS = [
73
+ ...OTP_SENSITIVE_FIELDS,
70
74
  // credentials / auth
71
75
  'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
72
76
  'access_token', 'auth', 'authorization', 'bearer', 'credentials',
@@ -124,7 +128,10 @@ function luhnValid(digits) {
124
128
  */
125
129
  function redactSensitiveValue(value) {
126
130
  if (typeof value !== 'string' || value.length === 0) return value;
127
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
131
+ // Credential assignments in GraphQL/query strings need field-aware
132
+ // redaction even though a short numeric OTP has no unique value shape.
133
+ value = redactOtpQuery(redactOtpGraphQL(value));
134
+ if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
128
135
  let out = value;
129
136
  for (const r of VALUE_REDACTORS) {
130
137
  r.re.lastIndex = 0;
@@ -150,9 +157,8 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
150
157
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
151
158
 
152
159
  for (const key of Object.keys(redacted)) {
153
- const lowerKey = key.toLowerCase();
154
160
 
155
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
161
+ if (matchesSensitiveField(key, sensitiveFields)) {
156
162
  redacted[key] = '[REDACTED]';
157
163
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
158
164
  // Recursively redact nested objects
@@ -181,7 +187,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
181
187
 
182
188
  // Redact sensitive fields in GraphQL arguments and variables
183
189
  // Matches patterns like: password: "value" or password:"value" or password:'value'
184
- sensitiveFields.forEach(field => {
190
+ sensitiveFields.filter(field => field === 'otp' || !isOtpCodeField(field)).forEach(field => {
185
191
  const escaped = escapeRegex(field);
186
192
  const patterns = [
187
193
  new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
@@ -201,7 +207,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
201
207
 
202
208
  // Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
203
209
  // body — catches values the key-name pass above can't see.
204
- redacted = redactSensitiveValue(redacted);
210
+ redacted = redactOtpGraphQL(redactSensitiveValue(redacted));
205
211
 
206
212
  return redacted;
207
213
  }
@@ -320,7 +326,7 @@ function registerSecureNow(options = {}) {
320
326
 
321
327
  // ======== REQUEST METADATA ========
322
328
  const userAgent = headers['user-agent'] || '';
323
- const referer = headers['referer'] || headers['referrer'] || '';
329
+ const referer = redactOtpQuery(headers['referer'] || headers['referrer'] || '');
324
330
  const accept = headers['accept'] || '';
325
331
  const acceptLanguage = headers['accept-language'] || '';
326
332
  const acceptEncoding = headers['accept-encoding'] || '';
@@ -329,7 +335,7 @@ function registerSecureNow(options = {}) {
329
335
  const origin = headers['origin'] || '';
330
336
 
331
337
  // ======== PROXY & LOAD BALANCER ========
332
- const originalUri = headers['x-original-uri'] || '';
338
+ const originalUri = redactOtpQuery(headers['x-original-uri'] || '');
333
339
  const originalMethod = headers['x-original-method'] || '';
334
340
  const requestId = headers['x-request-id'] || headers['x-trace-id'] || headers['x-correlation-id'] || '';
335
341
 
@@ -528,6 +534,7 @@ function registerSecureNow(options = {}) {
528
534
  const { registerOTel } = require('@vercel/otel');
529
535
 
530
536
  registerOTel({
537
+ traceExporter: withOtpRedactionExporter(new (require('@opentelemetry/exporter-trace-otlp-http').OTLPTraceExporter)({ url: tracesUrl, headers })),
531
538
  serviceName: serviceName,
532
539
  attributes: {
533
540
  'deployment.environment': deploymentEnvironment,
@@ -558,10 +565,10 @@ function registerSecureNow(options = {}) {
558
565
  const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
559
566
  const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
560
567
 
561
- const traceExporter = new OTLPTraceExporter({
568
+ const traceExporter = withOtpRedactionExporter(new OTLPTraceExporter({
562
569
  url: tracesUrl,
563
- headers
564
- });
570
+ headers
571
+ }));
565
572
 
566
573
  const sdk = new NodeSDK({
567
574
  ...nodeSdkTelemetryOptions,
@@ -643,7 +650,7 @@ function registerSecureNow(options = {}) {
643
650
  if (event === 'request' && req && res) {
644
651
  const start = Date.now();
645
652
  const method = req.method;
646
- const url = req.url;
653
+ const url = redactOtpQuery(req.url);
647
654
  res.on('finish', () => {
648
655
  const reqCtx = otelContext.active();
649
656
  const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
@@ -42,14 +42,18 @@ function createResource(attributes) {
42
42
  throw new Error('Unsupported @opentelemetry/resources version');
43
43
  }
44
44
 
45
+ const { OTP_SENSITIVE_FIELDS, matchesSensitiveField, redactOtpQuery, withOtpRedactionExporter, redactOtpGraphQL } = nodeRequire('./otp-redaction');
46
+
45
47
  // Default sensitive fields to redact from request bodies.
46
- // Matched substring-wise against lowercased keys (see redactSensitiveData),
48
+ // OTP/code aliases use exact normalized matching; other fields are matched
49
+ // substring-wise against lowercased keys (see redactSensitiveData),
47
50
  // so e.g. 'card' also catches 'creditCard' and 'account' also catches
48
51
  // 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
49
52
  // telemetry value is always safer than a leaked secret. Entries are kept
50
53
  // specific enough to avoid nuking broad benign keys (e.g. we use
51
54
  // 'firstname'/'lastname'/'fullname', never bare 'name').
52
55
  const DEFAULT_SENSITIVE_FIELDS = [
56
+ ...OTP_SENSITIVE_FIELDS,
53
57
  // credentials / auth
54
58
  'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
55
59
  'access_token', 'auth', 'authorization', 'bearer', 'credentials',
@@ -107,7 +111,10 @@ function luhnValid(digits) {
107
111
  */
108
112
  function redactSensitiveValue(value) {
109
113
  if (typeof value !== 'string' || value.length === 0) return value;
110
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
114
+ // Credential assignments in GraphQL/query strings need field-aware
115
+ // redaction even though a short numeric OTP has no unique value shape.
116
+ value = redactOtpQuery(redactOtpGraphQL(value));
117
+ if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
111
118
  let out = value;
112
119
  for (const r of VALUE_REDACTORS) {
113
120
  r.re.lastIndex = 0;
@@ -128,8 +135,7 @@ function redactSensitiveData(obj, fields = DEFAULT_SENSITIVE_FIELDS) {
128
135
  if (!obj || typeof obj !== 'object') return obj;
129
136
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
130
137
  for (const key of Object.keys(redacted)) {
131
- const lower = key.toLowerCase();
132
- if (fields.some((f) => lower.includes(f.toLowerCase()))) {
138
+ if (matchesSensitiveField(key, fields)) {
133
139
  redacted[key] = '[REDACTED]';
134
140
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
135
141
  redacted[key] = redactSensitiveData(redacted[key], fields);
@@ -299,7 +305,7 @@ export default defineNitroPlugin(async (nitroApp) => {
299
305
  }));
300
306
 
301
307
  // ── Trace exporter + SDK ──
302
- const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
308
+ const traceExporter = withOtpRedactionExporter(new OTLPTraceExporter({ url: tracesUrl, headers }));
303
309
 
304
310
  const sdk = new NodeSDK({
305
311
  ...nodeSdkTelemetryOptions,
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ // Match credential names exactly after case/separator normalization. Treating
4
+ // "code" as a substring would also hide status_code/error_code forensic data.
5
+ const OTP_SENSITIVE_FIELDS = Object.freeze([
6
+ 'code', 'otp', 'otpCode', 'oneTimeCode', 'oneTimePassword', 'passcode',
7
+ 'verificationCode', 'confirmationCode', 'emailCode', 'smsCode', 'loginCode',
8
+ 'signInCode', 'authCode', 'authenticationCode', 'mfaCode', 'totpCode', '2faCode',
9
+ 'securityCode', 'recoveryCode', 'backupCode',
10
+ ]);
11
+ const canonical = (key) => String(key).replace(/[^a-z0-9]/gi, '').toLowerCase();
12
+ const OTP_NAMES = new Set(OTP_SENSITIVE_FIELDS.map(canonical));
13
+
14
+ function isOtpCodeField(key) {
15
+ let name = String(key);
16
+ // Support individually captured header attributes and nested form keys.
17
+ name = name.replace(/^http\.(?:request|response)\.header[s]?\./i, '');
18
+ const nested = name.match(/(?:\[|\.)([^\[\].]+)\]?$/);
19
+ if (nested) name = nested[1];
20
+ name = name.replace(/^x[-_]/i, '');
21
+ return OTP_NAMES.has(canonical(name));
22
+ }
23
+
24
+ function matchesSensitiveField(key, fields) {
25
+ if (isOtpCodeField(key)) return true;
26
+ const lower = String(key).toLowerCase();
27
+ return fields.some((field) => (canonical(field) === 'otp' || !isOtpCodeField(field)) && lower.includes(String(field).toLowerCase()));
28
+ }
29
+
30
+ function redactOtpData(value) {
31
+ if (typeof value === 'string') return redactOtpQuery(redactOtpGraphQL(value));
32
+ if (!value || typeof value !== 'object') return value;
33
+ const out = Array.isArray(value) ? [...value] : { ...value };
34
+ for (const key of Object.keys(out)) {
35
+ out[key] = isOtpCodeField(key) ? '[REDACTED]' : redactOtpData(out[key]);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ function redactOtpGraphQL(query) {
41
+ return query.replace(/\b([A-Za-z_][\w]*)(\s*:\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,})]+)/g,
42
+ (match, key, separator) => isOtpCodeField(key) ? `${key}${separator}"[REDACTED]"` : match);
43
+ }
44
+
45
+ // Preserve the exact bytes of harmless query parameters, including attack
46
+ // payloads. Also covers duplicate and percent-encoded credential keys.
47
+ function redactOtpQuery(value, marker = 'REDACTED') {
48
+ if (typeof value !== 'string' || !value.includes('?')) return value;
49
+ return value.replace(/([?&])([^?&#=\s]+)=([^&#\s]*)/g, (part, separator, rawKey) => {
50
+ let key = rawKey;
51
+ try { key = decodeURIComponent(rawKey.replace(/\+/g, ' ')); } catch (_) {}
52
+ return isOtpCodeField(key) ? `${separator}${rawKey}=${marker}` : part;
53
+ });
54
+ }
55
+
56
+ function redactOtpAttributes(attributes = {}) {
57
+ const out = { ...attributes };
58
+ for (const [key, value] of Object.entries(out)) {
59
+ if (isOtpCodeField(key)) {
60
+ out[key] = '[REDACTED]';
61
+ } else if (typeof value === 'string') {
62
+ if (/^http\.(request|response)\.(body|headers|multipart\.fields)$/.test(key)) {
63
+ try {
64
+ // Scrub each string value before serializing. Running URL matching on
65
+ // the serialized JSON would consume closing quotes after query values.
66
+ out[key] = JSON.stringify(redactOtpData(JSON.parse(value)));
67
+ continue;
68
+ } catch (_) {}
69
+ }
70
+ out[key] = redactOtpQuery(out[key]);
71
+ // Stable URL semconv stores the query without a leading question mark.
72
+ if (key === 'url.query') out[key] = redactOtpQuery(`?${value}`).slice(1);
73
+ } else if (Array.isArray(value) && /^http\.(request|response)\.header\./.test(key)) {
74
+ out[key] = value.map((item) => redactOtpQuery(item));
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+
80
+ // Export a view rather than mutating the completed span or the real request.
81
+ // The final pass catches Next.js/Nitro/fetch attributes written after HTTP hooks.
82
+ function withOtpRedactionExporter(exporter) {
83
+ return {
84
+ export(spans, callback) {
85
+ return exporter.export(spans.map((span) => Object.create(span, {
86
+ attributes: { value: redactOtpAttributes(span.attributes), enumerable: true },
87
+ name: { value: redactOtpQuery(span.name), enumerable: true },
88
+ events: { value: (span.events || []).map((event) => ({ ...event, attributes: redactOtpAttributes(event.attributes) })), enumerable: true },
89
+ })), callback);
90
+ },
91
+ shutdown: () => exporter.shutdown(),
92
+ forceFlush: () => typeof exporter.forceFlush === 'function' ? exporter.forceFlush() : Promise.resolve(),
93
+ };
94
+ }
95
+
96
+ module.exports = {
97
+ OTP_SENSITIVE_FIELDS, isOtpCodeField, matchesSensitiveField, redactOtpData,
98
+ redactOtpGraphQL, redactOtpQuery, redactOtpAttributes, withOtpRedactionExporter,
99
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "8.11.1",
3
+ "version": "8.11.2",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",
@@ -10,7 +10,7 @@
10
10
  "securenow-mcp": "mcp/server.js"
11
11
  },
12
12
  "scripts": {
13
- "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-internal-thread.js && node test-package-contents.js"
13
+ "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-internal-thread.js && node test-package-contents.js"
14
14
  },
15
15
  "keywords": [
16
16
  "opentelemetry",
@@ -161,6 +161,7 @@
161
161
  "otel-defaults.js",
162
162
  "internal-thread.js",
163
163
  "legacy-semconv.js",
164
+ "otp-redaction.js",
164
165
  "rate-limits.js",
165
166
  "rate-limits.d.ts",
166
167
  "firewall-only.js",
package/tracing.js CHANGED
@@ -42,14 +42,18 @@ function createResource(attributes) {
42
42
  throw new Error('Unsupported @opentelemetry/resources version');
43
43
  }
44
44
 
45
+ const { OTP_SENSITIVE_FIELDS, isOtpCodeField, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery, withOtpRedactionExporter } = require('./otp-redaction');
46
+
45
47
  // Default sensitive fields to redact from request bodies.
46
- // Matched substring-wise against lowercased keys (see redactSensitiveData),
48
+ // OTP/code aliases use exact normalized matching; other fields are matched
49
+ // substring-wise against lowercased keys (see redactSensitiveData),
47
50
  // so e.g. 'card' also catches 'creditCard' and 'account' also catches
48
51
  // 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
49
52
  // telemetry value is always safer than a leaked secret. Entries are kept
50
53
  // specific enough to avoid nuking broad benign keys (e.g. we use
51
54
  // 'firstname'/'lastname'/'fullname', never bare 'name').
52
- const DEFAULT_SENSITIVE_FIELDS = [
55
+ const DEFAULT_SENSITIVE_FIELDS = [
56
+ ...OTP_SENSITIVE_FIELDS,
53
57
  // credentials / auth
54
58
  'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
55
59
  'access_token', 'auth', 'authorization', 'bearer', 'credentials',
@@ -107,7 +111,10 @@ function luhnValid(digits) {
107
111
  */
108
112
  function redactSensitiveValue(value) {
109
113
  if (typeof value !== 'string' || value.length === 0) return value;
110
- if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
114
+ // Credential assignments in GraphQL/query strings need field-aware
115
+ // redaction even though a short numeric OTP has no unique value shape.
116
+ value = redactOtpQuery(redactOtpGraphQL(value));
117
+ if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
111
118
  let out = value;
112
119
  for (const r of VALUE_REDACTORS) {
113
120
  r.re.lastIndex = 0;
@@ -137,9 +144,8 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
137
144
  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
138
145
 
139
146
  for (const key of Object.keys(redacted)) {
140
- const lowerKey = key.toLowerCase();
141
147
 
142
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
148
+ if (matchesSensitiveField(key, sensitiveFields)) {
143
149
  redacted[key] = '[REDACTED]';
144
150
  } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
145
151
  redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
@@ -162,7 +168,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
162
168
  let redacted = query;
163
169
 
164
170
  // Redact sensitive fields in GraphQL arguments and variables
165
- sensitiveFields.forEach(field => {
171
+ sensitiveFields.filter(field => field === 'otp' || !isOtpCodeField(field)).forEach(field => {
166
172
  const escaped = escapeRegex(field);
167
173
  const patterns = [
168
174
  new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
@@ -182,7 +188,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
182
188
 
183
189
  // Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
184
190
  // body — catches values the key-name pass above can't see.
185
- redacted = redactSensitiveValue(redacted);
191
+ redacted = redactOtpGraphQL(redactSensitiveValue(redacted));
186
192
 
187
193
  return redacted;
188
194
  }
@@ -226,8 +232,7 @@ function createMultipartMetaCollector(contentType, sensitiveFields, maxTextField
226
232
  if (isFile) {
227
233
  result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
228
234
  } else {
229
- const lower = fldName.toLowerCase();
230
- const redact = sensitiveFields.some(f => lower.includes(f.toLowerCase()));
235
+ const redact = matchesSensitiveField(fldName, sensitiveFields);
231
236
  // Name-match redaction first; otherwise scrub secret/PII *shapes* in the value.
232
237
  result.fields[fldName] = redact
233
238
  ? '[REDACTED]'
@@ -597,8 +602,8 @@ const httpInstrumentation = new HttpInstrumentation(withLegacyHttpSemconv({
597
602
  const SKIP_HEADERS = new Set(['cookie', 'authorization', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-auth-token']);
598
603
  const safe = {};
599
604
  for (const [k, v] of Object.entries(request.headers)) {
600
- if (SKIP_HEADERS.has(k.toLowerCase())) { safe[k] = '[REDACTED]'; continue; }
601
- safe[k] = typeof v === 'string' ? v.substring(0, 500) : String(v);
605
+ if (SKIP_HEADERS.has(k.toLowerCase()) || matchesSensitiveField(k, OTP_SENSITIVE_FIELDS)) { safe[k] = '[REDACTED]'; continue; }
606
+ safe[k] = redactOtpQuery(typeof v === 'string' ? v : String(v)).substring(0, 500);
602
607
  }
603
608
  const serialized = JSON.stringify(safe);
604
609
  if (serialized.length <= 8192) {
@@ -749,7 +754,7 @@ process.on('unhandledRejection', (reason) => {
749
754
  // -------- SDK --------
750
755
  // Contrib DB instrumentations >= 0.74 only emit stable db.* names; the wrapper
751
756
  // also exports the legacy db.statement/db.system/... aliases SecureNow reads.
752
- const traceExporter = withLegacyDbSemconvExporter(new OTLPTraceExporter({ url: tracesUrl, headers }));
757
+ const traceExporter = withOtpRedactionExporter(withLegacyDbSemconvExporter(new OTLPTraceExporter({ url: tracesUrl, headers })));
753
758
  const sdk = new NodeSDK({
754
759
  ...nodeSdkTelemetryOptions,
755
760
  traceExporter,