@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.
- package/lib/event-mappers.d.ts +3 -1
- package/lib/event-mappers.js +3 -3
- package/lib/server.d.ts +2 -0
- package/lib/server.js +1 -1
- package/lib/types.d.ts +31 -0
- package/package.json +3 -3
package/lib/event-mappers.d.ts
CHANGED
|
@@ -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,
|
|
27
|
+
export declare function envelopeToEvent(env: Exclude<ClientEnvelope, {
|
|
28
|
+
kind: 'batch';
|
|
29
|
+
}>, sessionId?: string): BaseEvent;
|
package/lib/event-mappers.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)[0]??e.reason;return{id:
|
|
3
|
-
`).slice(0,2);for(const
|
|
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
|
|
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.
|
|
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.
|
|
23
|
-
"@lensmcp/protocol-types": "1.18.
|
|
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
|
},
|