@lensmcp/core 1.21.5 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './lib/common.js';
2
2
  export * from './lib/ulid.js';
3
3
  export * from './lib/event-bus.js';
4
+ export * from './lib/event-redactor.js';
4
5
  export * from './lib/resource-store.js';
5
6
  export * from './lib/graph-store.js';
6
7
  export * from './lib/fingerprint.js';
package/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/resource-store.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/graph-from-events.js";
1
+ "use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/event-redactor.js";export*from"./lib/resource-store.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/graph-from-events.js";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Redaction as a property of the BUS, not of each producer.
3
+ *
4
+ * The previous design applied `@lensmcp/core`'s redactor per producer — `bridge/event-mappers.ts`
5
+ * and the two gateway `observability.ts` files — and nowhere else. Every other producer therefore
6
+ * opted out by default, which is how four separate disclosures reached disk and the dashboard:
7
+ *
8
+ * - the Postgres/Redis taps kept parameter values and returned rows (issues/codex-security/13),
9
+ * - the stdout tap copied whole log lines into event titles (36),
10
+ * - the Nest provider tracker put 40 characters of each cache/session key into `keyPreview` (40),
11
+ * - Valtio previews carried whole store values into `valtio://stores` (43).
12
+ *
13
+ * `grep redact libs/node-instrumentation/src/lib/` returned nothing. Fixing those four one at a time
14
+ * would have repeated the bug the next time a producer was added, because the default was wrong.
15
+ *
16
+ * Installed at `EventBus`'s `transform`, this runs on every event before storage and before all
17
+ * sixteen reducers, and no producer can bypass it.
18
+ */
19
+ import type { BaseEvent } from '@lensmcp/protocol-types';
20
+ import { MAX_TITLE_CHARS } from './redact.js';
21
+ /**
22
+ * Depth the bus redactor walks.
23
+ *
24
+ * The producer-side default is 6; the bus clamp keeps 12. A credential sitting between the two
25
+ * survived redaction on *every* path, so the bus matches the clamp: anything a consumer can see is
26
+ * something redaction has looked at.
27
+ */
28
+ export declare const BUS_REDACT_MAX_DEPTH = 12;
29
+ export interface EventRedactorStats {
30
+ /** Events inspected. */
31
+ inspected: number;
32
+ /** Events where at least one field changed. */
33
+ redacted: number;
34
+ }
35
+ export interface EventRedactor {
36
+ /** Redact one event. Returns the same object when nothing changed. */
37
+ redact(event: BaseEvent): BaseEvent;
38
+ stats(): EventRedactorStats;
39
+ reset(): void;
40
+ }
41
+ export interface EventRedactorOptions {
42
+ /** How deep to walk `raw`. Defaults to {@link BUS_REDACT_MAX_DEPTH}. */
43
+ maxDepth?: number;
44
+ }
45
+ /**
46
+ * A counting redactor for the bus `transform` seam.
47
+ *
48
+ * Covers `title`, `message` and `raw`. `title` and `message` matter as much as `raw` here: the
49
+ * structural clamp never covered them, so a stdout line promoted to a title had neither a bound nor
50
+ * a redaction pass (issues/codex-security/36). Both are clipped to their documented maxima on the
51
+ * way through, which is the bound that was missing.
52
+ */
53
+ export declare function createEventRedactor(opts?: EventRedactorOptions): EventRedactor;
54
+ /** Re-exported so a caller can reason about the title bound without importing two modules. */
55
+ export { MAX_TITLE_CHARS };
@@ -0,0 +1 @@
1
+ "use strict";var m=Object.defineProperty;var c=(e,a)=>m(e,"name",{value:a,configurable:!0});var p=Object.defineProperty,n=c((e,a)=>p(e,"name",{value:a,configurable:!0}),"t");import{MAX_MESSAGE_CHARS as _,MAX_TITLE_CHARS as E,redactDeep as f,redactText as v,safeText as u,safeTitle as x}from"./redact.js";export const BUS_REDACT_MAX_DEPTH=12;export function createEventRedactor(e={}){const a=e.maxDepth??BUS_REDACT_MAX_DEPTH;let r=0,s=0;return{redact(t){r+=1;const o=t.title===void 0?void 0:x(v(t.title)),i=t.message===void 0?void 0:u(v(t.message),_),d=t.raw===void 0?void 0:f(t.raw,0,a),A=o!==t.title,T=i!==t.message,l=d!==t.raw;return!A&&!T&&!l?t:(s+=1,{...t,...o!==void 0?{title:o}:{},...i!==void 0?{message:i}:{},...d!==void 0?{raw:d}:{}})},stats:n(()=>({inspected:r,redacted:s}),"stats"),reset:n(()=>{r=0,s=0},"reset")}}c(createEventRedactor,"createEventRedactor"),n(createEventRedactor,"createEventRedactor");export{E as MAX_TITLE_CHARS};
package/lib/redact.d.ts CHANGED
@@ -54,7 +54,16 @@ export declare const REDACTED = "REDACTED";
54
54
  export declare const MAX_TITLE_CHARS = 300;
55
55
  export declare const MAX_MESSAGE_CHARS = 4096;
56
56
  export declare const MAX_URL_CHARS = 1024;
57
- /** Is this query-parameter name carrying a credential? */
57
+ /**
58
+ * Is this query-parameter or property name carrying a credential?
59
+ *
60
+ * Deliberately NOT camelCase-aware. Splitting `sessionId` → `session`, `wsKey` → `key` redacts the
61
+ * identity fields the reducers and flow correlation key on, and `key` is ambiguous wherever it
62
+ * appears (`cacheKey`, `publicKey`, `wsKey`). A redactor guessing at field names is the weaker
63
+ * mechanism anyway: a producer that holds key material should emit a non-reversible hash instead of
64
+ * hoping the name looks suspicious — see `keyPreview` in `@lensmcp/memory-tracker`
65
+ * (issues/codex-security/40).
66
+ */
58
67
  export declare function isSecretParam(name: string): boolean;
59
68
  /**
60
69
  * Strip credentials from a URL — userinfo, then any query OR fragment
@@ -123,5 +132,9 @@ export declare function safeTitle(value: string): string;
123
132
  *
124
133
  * Values are returned by reference when nothing changed, so the common case
125
134
  * (no credential anywhere) allocates nothing.
135
+ *
136
+ * `maxDepth` defaults to the producer-side limit. The BUS redactor raises it to match the clamp's
137
+ * depth, because anything the clamp keeps is something a consumer will see — a credential sitting
138
+ * between the two limits survived redaction on every path (issues/codex-security/43).
126
139
  */
127
- export declare function redactDeep(value: unknown, depth?: number): unknown;
140
+ export declare function redactDeep(value: unknown, depth?: number, maxDepth?: number): unknown;
package/lib/redact.js CHANGED
@@ -1 +1 @@
1
- "use strict";var f=Object.defineProperty;var o=(e,r)=>f(e,"name",{value:r,configurable:!0});var l=Object.defineProperty,a=o((e,r)=>l(e,"name",{value:r,configurable:!0}),"o");export const REDACTED="REDACTED",MAX_TITLE_CHARS=300,MAX_MESSAGE_CHARS=4096,MAX_URL_CHARS=1024;const d=["token","secret","password","passwd","apikey","credential","assertion","signature"],x=new Set(["key","auth","authorization","jwt","code","sig","pwd","pass","session","sid","bearer","otp","cookie","set-cookie"]);export function isSecretParam(e){const r=e.toLowerCase();if(d.some(t=>r.includes(t)))return!0;for(const t of r.split(/[-_.]+/))if(t&&x.has(t))return!0;return!1}o(isSecretParam,"isSecretParam"),a(isSecretParam,"isSecretParam");const A=/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@\s]*@/i,$=/([?&#])([^=&#\s]+)=([^&#\s]*)/g;export function redactUrlParams(e){return e&&e.replace(A,`$1${REDACTED}@`).replace($,(r,t,s)=>isSecretParam(s)?`${t}${s}=${REDACTED}`:r)}o(redactUrlParams,"redactUrlParams"),a(redactUrlParams,"redactUrlParams");export function redactUrl(e){return e&&clip(redactUrlParams(e),1024)}o(redactUrl,"redactUrl"),a(redactUrl,"redactUrl");const _=/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g,b=/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,y=/\b(authorization|x-api-key|api-key|cookie|set-cookie|proxy-authorization)\b(\s*[:=]\s*)(["']?)([^\s"',;]{4,})\3/gi;export function redactText(e){return redactUrlParams(e).replace(y,(r,t,s,n)=>`${t}${s}${n}${REDACTED}${n}`).replace(_,REDACTED).replace(b,(r,t)=>`${t} ${REDACTED}`)}o(redactText,"redactText"),a(redactText,"redactText");export function clip(e,r){return e.length<=r?e:`${e.slice(0,r)}\u2026[lensmcp truncated: ${e.length-r} more chars]`}o(clip,"clip"),a(clip,"clip");export function safeText(e,r=4096){if(e!==void 0)return clip(redactText(e),r)}o(safeText,"safeText"),a(safeText,"safeText");export function safeTitle(e){return clip(redactText(e),300)}o(safeTitle,"safeTitle"),a(safeTitle,"safeTitle");const T=6,g=64;export function redactDeep(e,r=0){if(typeof e=="string")return redactText(e);if(e===null||typeof e!="object"||r>=T)return e;if(Array.isArray(e)){let n=!1;const i=e.map((c,p)=>{if(p>=g)return c;const u=redactDeep(c,r+1);return u!==c&&(n=!0),u});return n?i:e}let t=!1;const s={};for(const[n,i]of Object.entries(e)){const c=isSecretParam(n)&&i!==void 0&&i!==null?REDACTED:redactDeep(i,r+1);c!==i&&(t=!0),s[n]=c}return t?s:e}o(redactDeep,"redactDeep"),a(redactDeep,"redactDeep");
1
+ "use strict";var f=Object.defineProperty;var n=(e,r)=>f(e,"name",{value:r,configurable:!0});var d=Object.defineProperty,a=n((e,r)=>d(e,"name",{value:r,configurable:!0}),"n");export const REDACTED="REDACTED",MAX_TITLE_CHARS=300,MAX_MESSAGE_CHARS=4096,MAX_URL_CHARS=1024;const g=["token","secret","password","passwd","apikey","credential","assertion","signature"],m=new Set(["key","auth","authorization","jwt","code","sig","pwd","pass","session","sid","bearer","otp","cookie","set-cookie"]),x=/^(?:(?:prompt|completion|total|input|output|cache|cached|cacheread|cachewrite|reasoning|thinking|max|min|remaining|used|billed)tokens|tokens?(?:count|used|limit|remaining))$/;export function isSecretParam(e){const r=e.toLowerCase();if(x.test(r.replace(/[-_.]+/g,"")))return!1;if(g.some(t=>r.includes(t)))return!0;for(const t of r.split(/[-_.]+/))if(t&&m.has(t))return!0;return!1}n(isSecretParam,"isSecretParam"),a(isSecretParam,"isSecretParam");const A=/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@\s]*@/i,$=/([?&#])([^=&#\s]+)=([^&#\s]*)/g;export function redactUrlParams(e){return e&&e.replace(A,`$1${REDACTED}@`).replace($,(r,t,i)=>isSecretParam(i)?`${t}${i}=${REDACTED}`:r)}n(redactUrlParams,"redactUrlParams"),a(redactUrlParams,"redactUrlParams");export function redactUrl(e){return e&&clip(redactUrlParams(e),1024)}n(redactUrl,"redactUrl"),a(redactUrl,"redactUrl");const b=/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g,h=/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,_=/\b(authorization|x-api-key|api-key|cookie|set-cookie|proxy-authorization)\b(\s*[:=]\s*)(["']?)((?:(?:bearer|basic)\s+)?[^\s"',;]{4,})\3/gi;export function redactText(e){return redactUrlParams(e).replace(_,(r,t,i,c)=>`${t}${i}${c}${REDACTED}${c}`).replace(b,REDACTED).replace(h,(r,t)=>`${t} ${REDACTED}`)}n(redactText,"redactText"),a(redactText,"redactText");export function clip(e,r){return e.length<=r?e:`${e.slice(0,r)}\u2026[lensmcp truncated: ${e.length-r} more chars]`}n(clip,"clip"),a(clip,"clip");export function safeText(e,r=4096){if(e!==void 0)return clip(redactText(e),r)}n(safeText,"safeText"),a(safeText,"safeText");export function safeTitle(e){return clip(redactText(e),300)}n(safeTitle,"safeTitle"),a(safeTitle,"safeTitle");const k=6,y=64;export function redactDeep(e,r=0,t=k){if(typeof e=="string")return redactText(e);if(e===null||typeof e!="object"||r>=t)return e;if(Array.isArray(e)){let u=!1;const o=e.map((s,l)=>{if(l>=y)return s;const p=redactDeep(s,r+1,t);return p!==s&&(u=!0),p});return u?o:e}let i=!1;const c={};for(const[u,o]of Object.entries(e)){const s=isSecretParam(u)&&o!==void 0&&o!==null?REDACTED:redactDeep(o,r+1,t);s!==o&&(i=!0),c[u]=s}return i?c:e}n(redactDeep,"redactDeep"),a(redactDeep,"redactDeep");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/core",
3
- "version": "1.21.5",
3
+ "version": "1.22.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -19,7 +19,7 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@lensmcp/protocol-types": "1.21.5",
22
+ "@lensmcp/protocol-types": "1.22.0",
23
23
  "tslib": "^2.3.0"
24
24
  },
25
25
  "license": "Apache-2.0",