@lensmcp/bridge 1.18.6 → 1.18.8

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.
@@ -24,4 +24,6 @@ import type { ClientEnvelope } from './types.js';
24
24
  * (`./redact.js`) — enrichment is exactly what creates the leak risk, so the
25
25
  * two land together. See that module for what is and is not scrubbed.
26
26
  */
27
- export declare function envelopeToEvent(env: ClientEnvelope, sessionId?: string): BaseEvent;
27
+ export declare function envelopeToEvent(env: Exclude<ClientEnvelope, {
28
+ kind: 'batch';
29
+ }>, sessionId?: string): BaseEvent;
@@ -1,3 +1,3 @@
1
- "use strict";var h=Object.defineProperty;var c=(e,s)=>h(e,"name",{value:s,configurable:!0});var I=Object.defineProperty,l=c((e,s)=>I(e,"name",{value:s,configurable:!0}),"u");import{fingerprint as a,ulid as o}from"@lensmcp/core";import{redactDeep as g,redactText as $,redactUrl as d,safeText as u,safeTitle as m}from"./redact.js";const x="pending";export function envelopeToEvent(e,s=x){const n=e.tabId?{tabId:e.tabId}:{};switch(e.kind){case"error":{const r=!e.stack&&!e.source&&(!e.message||e.message==="Script error."),i=e.name&&e.name!=="Error"?`${e.name}: `:"",t=r?"Uncaught cross-origin script error (no detail available \u2014 add crossorigin to the script tag)":e.message?`${i}${e.message}`:`Uncaught ${e.name??"error"}`;return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:s,...n},fingerprint:a({kind:"runtime",identity:`error:${v(e.stack)}`,file:e.source,detail:e.message??e.name}),title:m(t),message:u(e.stack??e.message),location:e.source?{file:d(e.source),line:e.lineno,column:e.colno}:void 0,raw:{...e,...e.message?{message:$(e.message)}:{},...e.stack?{stack:u(e.stack)}:{},...e.source?{source:d(e.source)}:{}}}}case"resource-error":{const r=e.url?d(e.url):void 0;return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:s,...n},fingerprint:a({kind:"runtime",identity:`resource-error:${e.tagName}:${p(e.url??"")}`}),title:m(`Failed to load <${e.tagName}>${r?` ${r}`:""}`),...r?{relatedUrls:[r]}:{},raw:{...e,...r?{url:r}:{}}}}case"unhandledrejection":{const r=e.name&&e.name!=="Error"?`${e.name}: `:"",i=e.reason.split(`
2
- `)[0]??e.reason;return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:s,...n},fingerprint:a({kind:"runtime",identity:`unhandledrejection:${e.reason.slice(0,80)}`}),title:m(`Unhandled promise rejection: ${r}${i}`),message:u(e.stack??e.reason),raw:{...e,reason:$(e.reason),...e.stack?{stack:u(e.stack)}:{}}}}case"console":{const r=e.level==="error"?"error":e.level==="warn"?"warning":"info",i=g(e.args),t=i.map(w).join(" ");return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"runtime",severity:r,context:{sessionId:s,...n},fingerprint:a({kind:"runtime",identity:`console:${e.level}:${t.slice(0,80)}`}),title:`console.${e.level}`,message:u(t),raw:{...e,args:i}}}case"fetch":{const r=e.error!==void 0||e.status!==void 0&&e.status>=400,i=d(e.url),t=e.status!==void 0?` \u2192 ${e.status}${e.statusText?` ${e.statusText}`:""}`:"";return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"network",severity:r?"error":"info",context:{sessionId:s,...n},fingerprint:a({kind:"network",identity:`${e.method}:${p(e.url)}:${e.status??"err"}`}),title:m(`${e.method} ${i}${t}`),message:u(e.error),relatedUrls:[i],raw:{...e,url:i}}}case"route":return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:s,url:e.url,...n},fingerprint:a({kind:"runtime",identity:`route:${e.url}`}),title:`Route \u2192 ${e.url}`,raw:e};case"ready":return{id:o(),sessionId:s,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:s,url:e.url,...n},fingerprint:a({kind:"runtime",identity:"client-ready"}),title:"Client runtime ready",message:e.userAgent,raw:e};case"publish":{const r=k(e.category,e.raw),i=r&&r.status!==void 0&&r.status>=400?"error":e.severity??"info",t=l(y=>r&&r.url!==r.redactedUrl?y.split(r.url).join(r.redactedUrl):y,"scrub"),f=t(e.title);return{id:o(),sessionId:s,timestamp:e.at,source:e.source,category:e.category,severity:i,context:{sessionId:s,...n,...e.context??{}},fingerprint:e.fingerprint?t(e.fingerprint):a({kind:"publish",identity:f}),title:m(f),message:u(e.message),...r?{relatedUrls:[r.redactedUrl]}:{},raw:r?{...g(e.raw),url:r.redactedUrl}:g(e.raw)}}}}c(envelopeToEvent,"envelopeToEvent"),l(envelopeToEvent,"envelopeToEvent");function k(e,s){if(e!=="network"||!s||typeof s!="object")return;const n=s;if(!(typeof n.url!="string"||n.url.length===0))return{url:n.url,redactedUrl:d(n.url),...typeof n.status=="number"?{status:n.status}:{}}}c(k,"w"),l(k,"networkDetails");function v(e){if(!e)return"unknown";const s=e.split(`
3
- `).slice(0,2);for(const n of s){const r=n.match(/at\s+(\S+)/);if(r&&r[1])return r[1]}return"unknown"}c(v,"h"),l(v,"normaliseStackTopFrame");function p(e){try{return`${new URL(e,"http://x").pathname}`}catch{return e}}c(p,"y"),l(p,"normaliseUrl");function w(e){if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return String(e)}}c(w,"x"),l(w,"stringify");
1
+ "use strict";var I=Object.defineProperty;var c=(e,n)=>I(e,"name",{value:n,configurable:!0});var h=Object.defineProperty,l=c((e,n)=>h(e,"name",{value:n,configurable:!0}),"s");import{fingerprint as o,ulid as a}from"@lensmcp/core";import{redactDeep as g,redactText as $,redactUrl as d,safeText as u,safeTitle as m}from"./redact.js";const x="pending";export function envelopeToEvent(e,n=x){const s=e.tabId?{tabId:e.tabId}:{};switch(e.kind){case"error":{const r=!e.stack&&!e.source&&(!e.message||e.message==="Script error."),i=e.name&&e.name!=="Error"?`${e.name}: `:"",t=r?"Uncaught cross-origin script error (no detail available \u2014 add crossorigin to the script tag)":e.message?`${i}${e.message}`:`Uncaught ${e.name??"error"}`;return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:n,...s},fingerprint:o({kind:"runtime",identity:`error:${v(e.stack)}`,file:e.source,detail:e.message??e.name}),title:m(t),message:u(e.stack??e.message),location:e.source?{file:d(e.source),line:e.lineno,column:e.colno}:void 0,raw:{...e,...e.message?{message:$(e.message)}:{},...e.stack?{stack:u(e.stack)}:{},...e.source?{source:d(e.source)}:{}}}}case"resource-error":{const r=e.url?d(e.url):void 0;return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:n,...s},fingerprint:o({kind:"runtime",identity:`resource-error:${e.tagName}:${p(e.url??"")}`}),title:m(`Failed to load <${e.tagName}>${r?` ${r}`:""}`),...r?{relatedUrls:[r]}:{},raw:{...e,...r?{url:r}:{}}}}case"unhandledrejection":{const r=e.name&&e.name!=="Error"?`${e.name}: `:"",i=e.reason.split(`
2
+ `)[0]??e.reason;return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:n,...s},fingerprint:o({kind:"runtime",identity:`unhandledrejection:${e.reason.slice(0,80)}`}),title:m(`Unhandled promise rejection: ${r}${i}`),message:u(e.stack??e.reason),raw:{...e,reason:$(e.reason),...e.stack?{stack:u(e.stack)}:{}}}}case"console":{const r=e.level==="error"?"error":e.level==="warn"?"warning":"info",i=g(e.args),t=i.map(w).join(" ");return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:r,context:{sessionId:n,...s},fingerprint:o({kind:"runtime",identity:`console:${e.level}:${t.slice(0,80)}`}),title:`console.${e.level}`,message:u(t),raw:{...e,args:i}}}case"fetch":{const r=e.error!==void 0||e.status!==void 0&&e.status>=400,i=d(e.url),t=e.status!==void 0?` \u2192 ${e.status}${e.statusText?` ${e.statusText}`:""}`:"";return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"network",severity:r?"error":"info",context:{sessionId:n,...s},fingerprint:o({kind:"network",identity:`${e.method}:${p(e.url)}:${e.status??"err"}`}),title:m(`${e.method} ${i}${t}`),message:u(e.error),relatedUrls:[i],raw:{...e,url:i}}}case"route":return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:n,url:e.url,...s},fingerprint:o({kind:"runtime",identity:`route:${e.url}`}),title:`Route \u2192 ${e.url}`,raw:e};case"ready":return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:n,url:e.url,...s},fingerprint:o({kind:"runtime",identity:"client-ready"}),title:"Client runtime ready",message:e.userAgent,raw:e};case"gone":return{id:a(),sessionId:n,timestamp:e.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:n,...s},fingerprint:o({kind:"runtime",identity:"client-gone"}),title:"Client runtime gone",raw:e};case"publish":{const r=k(e.category,e.raw),i=r&&r.status!==void 0&&r.status>=400?"error":e.severity??"info",t=l(y=>r&&r.url!==r.redactedUrl?y.split(r.url).join(r.redactedUrl):y,"scrub"),f=t(e.title);return{id:a(),sessionId:n,timestamp:e.at,source:e.source,category:e.category,severity:i,context:{sessionId:n,...s,...e.context??{}},fingerprint:e.fingerprint?t(e.fingerprint):o({kind:"publish",identity:f}),title:m(f),message:u(e.message),...r?{relatedUrls:[r.redactedUrl]}:{},raw:r?{...g(e.raw),url:r.redactedUrl}:g(e.raw)}}}}c(envelopeToEvent,"envelopeToEvent"),l(envelopeToEvent,"envelopeToEvent");function k(e,n){if(e!=="network"||!n||typeof n!="object")return;const s=n;if(!(typeof s.url!="string"||s.url.length===0))return{url:s.url,redactedUrl:d(s.url),...typeof s.status=="number"?{status:s.status}:{}}}c(k,"w"),l(k,"networkDetails");function v(e){if(!e)return"unknown";const n=e.split(`
3
+ `).slice(0,2);for(const s of n){const r=s.match(/at\s+(\S+)/);if(r&&r[1])return r[1]}return"unknown"}c(v,"h"),l(v,"normaliseStackTopFrame");function p(e){try{return`${new URL(e,"http://x").pathname}`}catch{return e}}c(p,"y"),l(p,"normaliseUrl");function w(e){if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return String(e)}}c(w,"x"),l(w,"stringify");
package/lib/server.d.ts CHANGED
@@ -41,6 +41,8 @@ export declare function makePublisher(deps: BridgeDeps): (event: BaseEvent) => v
41
41
  * exercised over a real WebSocket without booting any bundler.
42
42
  */
43
43
  export declare function handleClientEnvelope(raw: string, publish: (event: BaseEvent) => void, sessionId?: string): void;
44
+ /** The tab a raw envelope belongs to, or undefined for a malformed/untagged one. */
45
+ export declare function readTabId(raw: string): string | undefined;
44
46
  /**
45
47
  * Stand up the browser-event bridge: a WebSocket server the injected
46
48
  * `@lensmcp/client-runtime` connects to, forwarding every envelope onto
package/lib/server.js CHANGED
@@ -1 +1 @@
1
- "use strict";var m=Object.defineProperty;var s=(r,o)=>m(r,"name",{value:o,configurable:!0});var v=Object.defineProperty,t=s((r,o)=>v(r,"name",{value:o,configurable:!0}),"t");import{createServer as d}from"node:net";import{WebSocketServer as b}from"ws";import{DEFAULT_WS_HOST as S,DEFAULT_WS_PORT as g}from"./constants.js";import{resolveEnvSink as h}from"./env-sink.js";import{envelopeToEvent as P}from"./event-mappers.js";export function makePublisher(r){let o;return e=>{if(r.bus){r.bus.publish(e);return}if(r.sink){r.sink(e);return}if(o===void 0&&(o=h()),o){o(e);return}(e.severity==="error"||e.severity==="fatal")&&console.debug(`[lensmcp-bridge] ${e.severity}: ${e.title}`)}}s(makePublisher,"makePublisher"),t(makePublisher,"makePublisher");export function handleClientEnvelope(r,o,e){let n;try{n=JSON.parse(r)}catch{return}!n||typeof n!="object"||!("kind"in n)||o(P(n,e))}s(handleClientEnvelope,"handleClientEnvelope"),t(handleClientEnvelope,"handleClientEnvelope");export async function createBridgeServer(r={}){const o=r.host??S,e=r.port??g,n=e===0?await findFreePort():e,a=makePublisher(r),i=new b({host:o,port:n});return i.on("connection",c=>{c.on("message",l=>{handleClientEnvelope(l.toString(),a,r.sessionId)})}),await new Promise((c,l)=>{const f=t(p=>{i.off("listening",u),l(p)},"onError"),u=t(()=>{i.off("error",f),c()},"onListening");i.once("error",f),i.once("listening",u)}),{port:n,host:o,close:t(()=>new Promise(c=>{i.close(()=>c())}),"close")}}s(createBridgeServer,"createBridgeServer"),t(createBridgeServer,"createBridgeServer");export function findFreePort(){return new Promise((r,o)=>{const e=d();e.unref(),e.on("error",o),e.listen(0,()=>{const n=e.address();if(!n){e.close(),o(new Error("Could not allocate free port"));return}const a=n.port;e.close(()=>r(a))})})}s(findFreePort,"findFreePort"),t(findFreePort,"findFreePort");
1
+ "use strict";var b=Object.defineProperty;var s=(r,e)=>b(r,"name",{value:e,configurable:!0});var v=Object.defineProperty,i=s((r,e)=>v(r,"name",{value:e,configurable:!0}),"i");import{createServer as m}from"node:net";import{WebSocketServer as g}from"ws";import{DEFAULT_WS_HOST as y,DEFAULT_WS_PORT as S}from"./constants.js";import{resolveEnvSink as h}from"./env-sink.js";import{envelopeToEvent as u}from"./event-mappers.js";export function makePublisher(r){let e;return t=>{if(r.bus){r.bus.publish(t);return}if(r.sink){r.sink(t);return}if(e===void 0&&(e=h()),e){e(t);return}(t.severity==="error"||t.severity==="fatal")&&console.debug(`[lensmcp-bridge] ${t.severity}: ${t.title}`)}}s(makePublisher,"makePublisher"),i(makePublisher,"makePublisher");export function handleClientEnvelope(r,e,t){let o;try{o=JSON.parse(r)}catch{return}if(!(!o||typeof o!="object"||!("kind"in o))){if(o.kind==="batch"){if(!Array.isArray(o.events))return;for(const n of o.events)!n||typeof n!="object"||!("kind"in n)||n.kind!=="batch"&&e(u(n,t));return}e(u(o,t))}}s(handleClientEnvelope,"handleClientEnvelope"),i(handleClientEnvelope,"handleClientEnvelope");export function readTabId(r){try{const e=JSON.parse(r);return typeof e?.tabId=="string"&&e.tabId?e.tabId:void 0}catch{return}}s(readTabId,"readTabId"),i(readTabId,"readTabId");export async function createBridgeServer(r={}){const e=r.host??y,t=r.port??S,o=t===0?await findFreePort():t,n=makePublisher(r),a=new g({host:e,port:o});return a.on("connection",c=>{let f;c.on("message",d=>{const l=d.toString();f??=readTabId(l),handleClientEnvelope(l,n,r.sessionId)}),c.on("close",()=>{f&&handleClientEnvelope(JSON.stringify({kind:"gone",at:Date.now(),tabId:f}),n,r.sessionId)})}),await new Promise((c,f)=>{const d=i(p=>{a.off("listening",l),f(p)},"onError"),l=i(()=>{a.off("error",d),c()},"onListening");a.once("error",d),a.once("listening",l)}),{port:o,host:e,close:i(()=>new Promise(c=>{a.close(()=>c())}),"close")}}s(createBridgeServer,"createBridgeServer"),i(createBridgeServer,"createBridgeServer");export function findFreePort(){return new Promise((r,e)=>{const t=m();t.unref(),t.on("error",e),t.listen(0,()=>{const o=t.address();if(!o){t.close(),e(new Error("Could not allocate free port"));return}const n=o.port;t.close(()=>r(n))})})}s(findFreePort,"findFreePort"),i(findFreePort,"findFreePort");
package/lib/types.d.ts CHANGED
@@ -79,6 +79,24 @@ export type ClientEnvelope = (
79
79
  at: number;
80
80
  userAgent: string;
81
81
  url: string;
82
+ }
83
+ /**
84
+ * The tab's transport closed — its document is GONE.
85
+ *
86
+ * Synthesised by the BRIDGE on socket close, never sent by the page: the
87
+ * failure this exists for is exactly the one a page cannot report. `ready`
88
+ * comes from the injected client runtime, so a tab whose entry script failed
89
+ * to load can never announce a document boundary — and under `ready`-only
90
+ * retraction its error was immortal by construction.
91
+ *
92
+ * Measured on foodguard (2026-08-01): 38 retained browser errors across 26
93
+ * DISTINCT tabs, none with more than two — 26 dead one-shot documents, 15 of
94
+ * them the client script itself failing to load. `agent://current-status`
95
+ * reported `failing` on all of it while the app served cleanly.
96
+ */
97
+ | {
98
+ kind: 'gone';
99
+ at: number;
82
100
  } | {
83
101
  kind: 'publish';
84
102
  at: number;
@@ -90,6 +108,19 @@ export type ClientEnvelope = (
90
108
  fingerprint?: string;
91
109
  context?: Record<string, unknown>;
92
110
  raw?: unknown;
111
+ }
112
+ /**
113
+ * A COALESCED frame: every envelope the page produced within one flush
114
+ * window (a macrotask), in capture order. The client runtime batches so a
115
+ * render storm costs ONE stringify + ONE socket frame scheduled OFF the
116
+ * interaction task instead of hundreds inline in it — each inner event
117
+ * still carries its own capture-time `at` (and `tabId`), so the recorded
118
+ * timeline is unchanged; only where the transport work happens moves.
119
+ */
120
+ | {
121
+ kind: 'batch';
122
+ at: number;
123
+ events: ClientEnvelope[];
93
124
  }) & {
94
125
  /** Per-tab identity stamped by the client runtime (sessionStorage is per-tab) — keys `browser://tabs`. */
95
126
  tabId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/bridge",
3
- "version": "1.18.6",
3
+ "version": "1.18.8",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -19,8 +19,8 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@lensmcp/core": "1.18.6",
23
- "@lensmcp/protocol-types": "1.18.6",
22
+ "@lensmcp/core": "1.18.8",
23
+ "@lensmcp/protocol-types": "1.18.8",
24
24
  "tslib": "^2.3.0",
25
25
  "ws": "^8.21.0"
26
26
  },