@lensmcp/bridge 1.18.3 → 1.18.6
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 +1 -1
- package/index.js +1 -4
- package/lib/constants.d.ts +0 -1
- package/lib/constants.js +1 -24
- package/lib/env-sink.d.ts +0 -1
- package/lib/env-sink.js +3 -203
- package/lib/event-mappers.d.ts +18 -1
- package/lib/event-mappers.js +3 -169
- package/lib/redact.d.ts +20 -0
- package/lib/redact.js +1 -0
- package/lib/server.d.ts +0 -1
- package/lib/server.js +1 -109
- package/lib/types.d.ts +38 -4
- package/lib/types.js +1 -1
- package/main.d.ts +0 -1
- package/main.js +1 -47
- package/package.json +3 -3
- package/index.d.ts.map +0 -1
- package/lib/constants.d.ts.map +0 -1
- package/lib/env-sink.d.ts.map +0 -1
- package/lib/event-mappers.d.ts.map +0 -1
- package/lib/server.d.ts.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/main.d.ts.map +0 -1
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createBridgeServer, makePublisher, handleClientEnvelope, findFreePort, type BridgeServer, type BridgeServerOptions, type BridgeDeps, } from './lib/server.js';
|
|
2
2
|
export { envelopeToEvent } from './lib/event-mappers.js';
|
|
3
|
+
export { redactUrl, redactText, redactDeep, isSecretParam, clip, safeText, safeTitle, REDACTED, MAX_TITLE_CHARS, MAX_MESSAGE_CHARS, MAX_URL_CHARS, } from './lib/redact.js';
|
|
3
4
|
export { resolveEnvSink, resetEnvSinkCache, udsEnvSink } from './lib/env-sink.js';
|
|
4
5
|
export { DEFAULT_WS_HOST, DEFAULT_WS_PORT, ENV_WS_HOST, ENV_WS_PORT, ENV_SESSION_ID, } from './lib/constants.js';
|
|
5
6
|
export type { ClientEnvelope, EnvSink } from './lib/types.js';
|
|
6
|
-
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export { envelopeToEvent } from './lib/event-mappers.js';
|
|
3
|
-
export { resolveEnvSink, resetEnvSinkCache, udsEnvSink } from './lib/env-sink.js';
|
|
4
|
-
export { DEFAULT_WS_HOST, DEFAULT_WS_PORT, ENV_WS_HOST, ENV_WS_PORT, ENV_SESSION_ID, } from './lib/constants.js';
|
|
1
|
+
"use strict";export{createBridgeServer,makePublisher,handleClientEnvelope,findFreePort}from"./lib/server.js";export{envelopeToEvent}from"./lib/event-mappers.js";export{redactUrl,redactText,redactDeep,isSecretParam,clip,safeText,safeTitle,REDACTED,MAX_TITLE_CHARS,MAX_MESSAGE_CHARS,MAX_URL_CHARS}from"./lib/redact.js";export{resolveEnvSink,resetEnvSinkCache,udsEnvSink}from"./lib/env-sink.js";export{DEFAULT_WS_HOST,DEFAULT_WS_PORT,ENV_WS_HOST,ENV_WS_PORT,ENV_SESSION_ID}from"./lib/constants.js";
|
package/lib/constants.d.ts
CHANGED
|
@@ -22,4 +22,3 @@ export declare const ENV_WS_HOST = "LENSMCP_WS_HOST";
|
|
|
22
22
|
export declare const ENV_WS_PORT = "LENSMCP_WS_PORT";
|
|
23
23
|
/** Env var: session id stamped onto every event the bridge publishes. */
|
|
24
24
|
export declare const ENV_SESSION_ID = "LENSMCP_SESSION_ID";
|
|
25
|
-
//# sourceMappingURL=constants.d.ts.map
|
package/lib/constants.js
CHANGED
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared transport constants for the LensMCP browser-event bridge.
|
|
3
|
-
*
|
|
4
|
-
* The bridge is the single WebSocket endpoint that the injected
|
|
5
|
-
* `@lensmcp/client-runtime` posts envelopes to. For the Vite plugin this
|
|
6
|
-
* endpoint is born inside the dev server on an ephemeral port; for every
|
|
7
|
-
* other host (webpack, Next.js, no-build) it runs as a standalone sidecar
|
|
8
|
-
* on a FIXED, env-overridable port — so the browser runtime can find it
|
|
9
|
-
* without a build step baking the URL in.
|
|
10
|
-
*
|
|
11
|
-
* `DEFAULT_WS_PORT` must agree between the standalone bridge runner
|
|
12
|
-
* (`@lensmcp/bridge`) and the no-build client entry
|
|
13
|
-
* (`@lensmcp/client-runtime/auto`), which lives in a separate
|
|
14
|
-
* browser-targeted package and re-declares the same literal.
|
|
15
|
-
*/
|
|
16
|
-
export const DEFAULT_WS_HOST = '127.0.0.1';
|
|
17
|
-
/** Default fixed port for the standalone bridge sidecar. Override via `LENSMCP_WS_PORT`. */
|
|
18
|
-
export const DEFAULT_WS_PORT = 5747;
|
|
19
|
-
/** Env var: bind host for the standalone bridge. */
|
|
20
|
-
export const ENV_WS_HOST = 'LENSMCP_WS_HOST';
|
|
21
|
-
/** Env var: bind port for the standalone bridge (`0` selects an ephemeral port). */
|
|
22
|
-
export const ENV_WS_PORT = 'LENSMCP_WS_PORT';
|
|
23
|
-
/** Env var: session id stamped onto every event the bridge publishes. */
|
|
24
|
-
export const ENV_SESSION_ID = 'LENSMCP_SESSION_ID';
|
|
1
|
+
"use strict";export const DEFAULT_WS_HOST="127.0.0.1",DEFAULT_WS_PORT=5747,ENV_WS_HOST="LENSMCP_WS_HOST",ENV_WS_PORT="LENSMCP_WS_PORT",ENV_SESSION_ID="LENSMCP_SESSION_ID";
|
package/lib/env-sink.d.ts
CHANGED
|
@@ -4,4 +4,3 @@ export declare function resolveEnvSink(): EnvSink | null;
|
|
|
4
4
|
export declare function resetEnvSinkCache(): void;
|
|
5
5
|
/** Reconnecting NDJSON client over a Unix domain socket (see session/uds.ts). */
|
|
6
6
|
export declare function udsEnvSink(path: string): EnvSink;
|
|
7
|
-
//# sourceMappingURL=env-sink.d.ts.map
|
package/lib/env-sink.js
CHANGED
|
@@ -1,203 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// ---- file-sink hygiene — mirrors @lensmcp/node-instrumentation's emit.ts contract ------------------
|
|
5
|
-
// (bridge deliberately has no dep on node-instrumentation, so the ~40 lines are mirrored, same env
|
|
6
|
-
// vars.) Browser producers are the highest-volume writers on the bus — a valtio store churning per
|
|
7
|
-
// frame or a fetch retry loop must not grow events.jsonl unboundedly (2026-07-28: ~680 ev/s, 254 MB
|
|
8
|
-
// in 2h, every ingesting consumer OOM-crash-looped). Per-fingerprint token bucket + round-robin
|
|
9
|
-
// rotation: live file caps at MAX_EVENT_BYTES, history kept as `.1` … `.N`, oldest dropped.
|
|
10
|
-
/** Same cap contract as emit.ts: `LENSMCP_EVENT_MAX_BYTES` (default 50 MB) per generation. */
|
|
11
|
-
const MAX_EVENT_BYTES = (() => {
|
|
12
|
-
const n = Number(process.env['LENSMCP_EVENT_MAX_BYTES']);
|
|
13
|
-
return Number.isFinite(n) && n > 0 ? n : 50 * 1024 * 1024;
|
|
14
|
-
})();
|
|
15
|
-
/** Same history contract as emit.ts: `LENSMCP_EVENT_MAX_FILES` rotated generations (default 5). */
|
|
16
|
-
const MAX_EVENT_FILES = (() => {
|
|
17
|
-
const n = Number(process.env['LENSMCP_EVENT_MAX_FILES']);
|
|
18
|
-
return Number.isFinite(n) && n >= 1 && n <= 20 ? Math.floor(n) : 5;
|
|
19
|
-
})();
|
|
20
|
-
const ROTATE_CHECK_BYTES = 1024 * 1024; // stat at most ~once per MB appended
|
|
21
|
-
let bytesSinceCheck = 0;
|
|
22
|
-
function rotateIfLarge(file, addedBytes) {
|
|
23
|
-
bytesSinceCheck += addedBytes;
|
|
24
|
-
if (bytesSinceCheck < ROTATE_CHECK_BYTES)
|
|
25
|
-
return;
|
|
26
|
-
bytesSinceCheck = 0;
|
|
27
|
-
try {
|
|
28
|
-
if (statSync(file).size <= MAX_EVENT_BYTES)
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return; /* missing / already rotated by a concurrent writer — fine */
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
unlinkSync(`${file}.${MAX_EVENT_FILES}`); // drop the oldest generation
|
|
36
|
-
}
|
|
37
|
-
catch { /* none to drop */ }
|
|
38
|
-
for (let gen = MAX_EVENT_FILES - 1; gen >= 1; gen--) {
|
|
39
|
-
try {
|
|
40
|
-
renameSync(`${file}.${gen}`, `${file}.${gen + 1}`);
|
|
41
|
-
}
|
|
42
|
-
catch { /* gap in history */ }
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
renameSync(file, `${file}.1`);
|
|
46
|
-
}
|
|
47
|
-
catch { /* lost the rotate race — a concurrent cascade ran */ }
|
|
48
|
-
}
|
|
49
|
-
/** Per-fingerprint token bucket (burst 30, then 5/s; `LENSMCP_EVENT_MAX_EPS` overrides, 0/'off' disables). */
|
|
50
|
-
const FLOOD_PER_SEC = (() => {
|
|
51
|
-
const raw = process.env['LENSMCP_EVENT_MAX_EPS'];
|
|
52
|
-
if (raw === 'off')
|
|
53
|
-
return Infinity;
|
|
54
|
-
const n = Number(raw);
|
|
55
|
-
if (raw !== undefined && raw !== '' && Number.isFinite(n))
|
|
56
|
-
return n <= 0 ? Infinity : n;
|
|
57
|
-
return 5;
|
|
58
|
-
})();
|
|
59
|
-
const FLOOD_BURST = Math.max(30, FLOOD_PER_SEC === Infinity ? 0 : FLOOD_PER_SEC * 4);
|
|
60
|
-
const floodBuckets = new Map();
|
|
61
|
-
/** Returns `-1` to drop, else the suppressed-since-last-admit count to annotate (0 normally). */
|
|
62
|
-
function floodAdmit(fingerprint) {
|
|
63
|
-
if (FLOOD_PER_SEC === Infinity)
|
|
64
|
-
return 0;
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
let b = floodBuckets.get(fingerprint);
|
|
67
|
-
if (!b) {
|
|
68
|
-
if (floodBuckets.size >= 2048) {
|
|
69
|
-
for (const [k, v] of floodBuckets)
|
|
70
|
-
if (now - v.lastSeenAt > 60_000)
|
|
71
|
-
floodBuckets.delete(k);
|
|
72
|
-
if (floodBuckets.size >= 2048)
|
|
73
|
-
floodBuckets.clear();
|
|
74
|
-
}
|
|
75
|
-
b = { tokens: FLOOD_BURST, lastRefillAt: now, suppressed: 0, lastSeenAt: now };
|
|
76
|
-
floodBuckets.set(fingerprint, b);
|
|
77
|
-
}
|
|
78
|
-
b.lastSeenAt = now;
|
|
79
|
-
b.tokens = Math.min(FLOOD_BURST, b.tokens + ((now - b.lastRefillAt) / 1000) * FLOOD_PER_SEC);
|
|
80
|
-
b.lastRefillAt = now;
|
|
81
|
-
if (b.tokens >= 1) {
|
|
82
|
-
b.tokens -= 1;
|
|
83
|
-
const suppressed = b.suppressed;
|
|
84
|
-
b.suppressed = 0;
|
|
85
|
-
return suppressed;
|
|
86
|
-
}
|
|
87
|
-
b.suppressed += 1;
|
|
88
|
-
return -1;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Resolve a cross-process event sink from env, mirroring
|
|
92
|
-
* `@lensmcp/nest-instrumentation`'s `defaultEventSink` so every producer
|
|
93
|
-
* (Nest app, Vite plugin, standalone bridge) speaks the same transport:
|
|
94
|
-
* 1. `LENSMCP_EVENT_FILE` — append one JSON line per event.
|
|
95
|
-
* 2. `LENSMCP_UDS` — Unix domain socket, framed NDJSON, reconnecting.
|
|
96
|
-
* 3. `LENSMCP_IPC_SOCKET` (`host:port`) — one UDP datagram per event.
|
|
97
|
-
* Cached after first resolution; returns `null` when none is set.
|
|
98
|
-
*/
|
|
99
|
-
let cachedEnvSink;
|
|
100
|
-
export function resolveEnvSink() {
|
|
101
|
-
if (cachedEnvSink !== undefined)
|
|
102
|
-
return cachedEnvSink;
|
|
103
|
-
const filePath = process.env['LENSMCP_EVENT_FILE'];
|
|
104
|
-
if (filePath) {
|
|
105
|
-
cachedEnvSink = (event) => {
|
|
106
|
-
try {
|
|
107
|
-
const suppressed = floodAdmit(event.fingerprint);
|
|
108
|
-
if (suppressed < 0)
|
|
109
|
-
return; // flood-guarded — next admitted event carries the count
|
|
110
|
-
const line = JSON.stringify(suppressed > 0 ? { ...event, raw: { ...(event.raw ?? {}), floodSuppressed: suppressed } } : event) + '\n';
|
|
111
|
-
rotateIfLarge(filePath, line.length);
|
|
112
|
-
appendFileSync(filePath, line);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
/* swallow */
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
return cachedEnvSink;
|
|
119
|
-
}
|
|
120
|
-
const udsPath = process.env['LENSMCP_UDS'];
|
|
121
|
-
if (udsPath) {
|
|
122
|
-
cachedEnvSink = udsEnvSink(udsPath);
|
|
123
|
-
return cachedEnvSink;
|
|
124
|
-
}
|
|
125
|
-
const socketPath = process.env['LENSMCP_IPC_SOCKET'];
|
|
126
|
-
if (socketPath) {
|
|
127
|
-
const [host, portStr] = socketPath.split(':');
|
|
128
|
-
const port = Number(portStr);
|
|
129
|
-
if (host && Number.isFinite(port)) {
|
|
130
|
-
const sock = createSocket('udp4');
|
|
131
|
-
sock.unref();
|
|
132
|
-
cachedEnvSink = (event) => {
|
|
133
|
-
try {
|
|
134
|
-
const buf = Buffer.from(JSON.stringify(event));
|
|
135
|
-
sock.send(buf, port, host, () => undefined);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
/* swallow */
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
return cachedEnvSink;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
cachedEnvSink = null;
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
/** Test/embedding hook: drop the memoised sink so the next resolve re-reads env. */
|
|
148
|
-
export function resetEnvSinkCache() {
|
|
149
|
-
cachedEnvSink = undefined;
|
|
150
|
-
}
|
|
151
|
-
/** Reconnecting NDJSON client over a Unix domain socket (see session/uds.ts). */
|
|
152
|
-
export function udsEnvSink(path) {
|
|
153
|
-
const queue = [];
|
|
154
|
-
let sock = null;
|
|
155
|
-
let connecting = false;
|
|
156
|
-
const flush = () => {
|
|
157
|
-
if (!sock || !sock.writable)
|
|
158
|
-
return;
|
|
159
|
-
while (queue.length > 0)
|
|
160
|
-
sock.write(queue.shift());
|
|
161
|
-
};
|
|
162
|
-
const open = () => {
|
|
163
|
-
if (connecting || sock)
|
|
164
|
-
return;
|
|
165
|
-
connecting = true;
|
|
166
|
-
const s = connect(path);
|
|
167
|
-
s.on('connect', () => {
|
|
168
|
-
connecting = false;
|
|
169
|
-
sock = s;
|
|
170
|
-
flush();
|
|
171
|
-
});
|
|
172
|
-
s.on('drain', flush);
|
|
173
|
-
s.on('error', () => {
|
|
174
|
-
connecting = false;
|
|
175
|
-
sock = null;
|
|
176
|
-
});
|
|
177
|
-
s.on('close', () => {
|
|
178
|
-
sock = null;
|
|
179
|
-
});
|
|
180
|
-
s.unref();
|
|
181
|
-
};
|
|
182
|
-
open();
|
|
183
|
-
return (event) => {
|
|
184
|
-
queue.push(JSON.stringify(event) + '\n');
|
|
185
|
-
while (queue.length > 10000)
|
|
186
|
-
queue.shift();
|
|
187
|
-
if (sock && sock.writable) {
|
|
188
|
-
flush();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
// Dead socket (server restart) — drop it so open() reconnects.
|
|
192
|
-
if (sock) {
|
|
193
|
-
try {
|
|
194
|
-
sock.destroy();
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
/* ignore */
|
|
198
|
-
}
|
|
199
|
-
sock = null;
|
|
200
|
-
}
|
|
201
|
-
open();
|
|
202
|
-
};
|
|
203
|
-
}
|
|
1
|
+
"use strict";var y=Object.defineProperty;var f=(n,t)=>y(n,"name",{value:t,configurable:!0});var k=Object.defineProperty,c=f((n,t)=>k(n,"name",{value:t,configurable:!0}),"i");import{connect as _}from"node:net";import{createSocket as M}from"node:dgram";import{appendFileSync as b,renameSync as S,statSync as A,unlinkSync as C}from"node:fs";const g=(()=>{const n=Number(process.env.LENSMCP_EVENT_MAX_BYTES);return Number.isFinite(n)&&n>0?n:50*1024*1024})(),E=(()=>{const n=Number(process.env.LENSMCP_EVENT_MAX_FILES);return Number.isFinite(n)&&n>=1&&n<=20?Math.floor(n):5})(),L=1024*1024;let p=0;function d(n,t){if(p+=t,!(p<L)){p=0;try{if(A(n).size<=g)return}catch{return}try{C(`${n}.${E}`)}catch{}for(let e=E-1;e>=1;e--)try{S(`${n}.${e}`,`${n}.${e+1}`)}catch{}try{S(n,`${n}.1`)}catch{}}}f(d,"C"),c(d,"rotateIfLarge");const a=(()=>{const n=process.env.LENSMCP_EVENT_MAX_EPS;if(n==="off")return 1/0;const t=Number(n);return n!==void 0&&n!==""&&Number.isFinite(t)?t<=0?1/0:t:5})(),h=Math.max(30,a===1/0?0:a*4),l=new Map;function v(n){if(a===1/0)return 0;const t=Date.now();let e=l.get(n);if(!e){if(l.size>=2048){for(const[r,s]of l)t-s.lastSeenAt>6e4&&l.delete(r);l.size>=2048&&l.clear()}e={tokens:h,lastRefillAt:t,suppressed:0,lastSeenAt:t},l.set(n,e)}if(e.lastSeenAt=t,e.tokens=Math.min(h,e.tokens+(t-e.lastRefillAt)/1e3*a),e.lastRefillAt=t,e.tokens>=1){e.tokens-=1;const r=e.suppressed;return e.suppressed=0,r}return e.suppressed+=1,-1}f(v,"T"),c(v,"floodAdmit");let i;export function resolveEnvSink(){if(i!==void 0)return i;const n=process.env.LENSMCP_EVENT_FILE;if(n)return i=c(r=>{try{const s=v(r.fingerprint);if(s<0)return;const u=JSON.stringify(s>0?{...r,raw:{...r.raw??{},floodSuppressed:s}}:r)+`
|
|
2
|
+
`;d(n,u.length),b(n,u)}catch{}},"cachedEnvSink"),i;const t=process.env.LENSMCP_UDS;if(t)return i=udsEnvSink(t),i;const e=process.env.LENSMCP_IPC_SOCKET;if(e){const[r,s]=e.split(":"),u=Number(s);if(r&&Number.isFinite(u)){const o=M("udp4");return o.unref(),i=c(N=>{try{const m=Buffer.from(JSON.stringify(N));o.send(m,u,r,()=>{})}catch{}},"cachedEnvSink"),i}}return i=null,null}f(resolveEnvSink,"resolveEnvSink"),c(resolveEnvSink,"resolveEnvSink");export function resetEnvSinkCache(){i=void 0}f(resetEnvSinkCache,"resetEnvSinkCache"),c(resetEnvSinkCache,"resetEnvSinkCache");export function udsEnvSink(n){const t=[];let e=null,r=!1;const s=c(()=>{if(!(!e||!e.writable))for(;t.length>0;)e.write(t.shift())},"flush"),u=c(()=>{if(r||e)return;r=!0;const o=_(n);o.on("connect",()=>{r=!1,e=o,s()}),o.on("drain",s),o.on("error",()=>{r=!1,e=null}),o.on("close",()=>{e=null}),o.unref()},"open");return u(),o=>{for(t.push(JSON.stringify(o)+`
|
|
3
|
+
`);t.length>1e4;)t.shift();if(e&&e.writable){s();return}if(e){try{e.destroy()}catch{}e=null}u()}}f(udsEnvSink,"udsEnvSink"),c(udsEnvSink,"udsEnvSink");
|
package/lib/event-mappers.d.ts
CHANGED
|
@@ -5,6 +5,23 @@ import type { ClientEnvelope } from './types.js';
|
|
|
5
5
|
* `BaseEvent` consumed by the LensMCP event bus. The bridge publishes
|
|
6
6
|
* these; reducers in `@lensmcp/<app>` materialise them into
|
|
7
7
|
* `runtime://browser/*` resources.
|
|
8
|
+
*
|
|
9
|
+
* ── Substance is the point ──────────────────────────────────────────────────
|
|
10
|
+
* This mapper decides what a reader of `runtime://browser/errors` can actually
|
|
11
|
+
* learn. Measured on foodguard (2026-07-30) an entry looked like:
|
|
12
|
+
*
|
|
13
|
+
* { id, timestamp, severity: 'error', source: 'client-runtime',
|
|
14
|
+
* fingerprint: 'runtime:52331dbd', tabId: 'tab-gal0r3bs',
|
|
15
|
+
* raw: { kind: 'error', at: 1785450203325, tabId: 'tab-gal0r3bs' } }
|
|
16
|
+
*
|
|
17
|
+
* — no message, no stack, no URL, no status. The resource could report that
|
|
18
|
+
* two errors happened and nothing whatsoever about what they were. A signal
|
|
19
|
+
* that is correct but undiagnosable is only half a signal, so every branch
|
|
20
|
+
* below now carries identity: a title that names the failure, a bounded
|
|
21
|
+
* message/stack, and for a request its method, URL and STATUS.
|
|
22
|
+
*
|
|
23
|
+
* Every string that reaches `title`/`message` is redacted and bounded first
|
|
24
|
+
* (`./redact.js`) — enrichment is exactly what creates the leak risk, so the
|
|
25
|
+
* two land together. See that module for what is and is not scrubbed.
|
|
8
26
|
*/
|
|
9
27
|
export declare function envelopeToEvent(env: ClientEnvelope, sessionId?: string): BaseEvent;
|
|
10
|
-
//# sourceMappingURL=event-mappers.d.ts.map
|
package/lib/event-mappers.js
CHANGED
|
@@ -1,169 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
* Translate a single client-runtime envelope into the normalised
|
|
5
|
-
* `BaseEvent` consumed by the LensMCP event bus. The bridge publishes
|
|
6
|
-
* these; reducers in `@lensmcp/<app>` materialise them into
|
|
7
|
-
* `runtime://browser/*` resources.
|
|
8
|
-
*/
|
|
9
|
-
export function envelopeToEvent(env, sessionId = SESSION_ID_PLACEHOLDER) {
|
|
10
|
-
// Per-tab identity stamped by the client runtime — keys browser://tabs.
|
|
11
|
-
const tab = env.tabId ? { tabId: env.tabId } : {};
|
|
12
|
-
switch (env.kind) {
|
|
13
|
-
case 'error':
|
|
14
|
-
return {
|
|
15
|
-
id: ulid(),
|
|
16
|
-
sessionId,
|
|
17
|
-
timestamp: env.at,
|
|
18
|
-
source: 'client-runtime',
|
|
19
|
-
category: 'runtime',
|
|
20
|
-
severity: 'error',
|
|
21
|
-
context: { sessionId, ...tab },
|
|
22
|
-
fingerprint: fingerprint({
|
|
23
|
-
kind: 'runtime',
|
|
24
|
-
identity: `error:${normaliseStackTopFrame(env.stack)}`,
|
|
25
|
-
file: env.source,
|
|
26
|
-
detail: env.message,
|
|
27
|
-
}),
|
|
28
|
-
title: env.message,
|
|
29
|
-
message: env.stack ?? env.message,
|
|
30
|
-
location: env.source
|
|
31
|
-
? { file: env.source, line: env.lineno, column: env.colno }
|
|
32
|
-
: undefined,
|
|
33
|
-
raw: env,
|
|
34
|
-
};
|
|
35
|
-
case 'unhandledrejection':
|
|
36
|
-
return {
|
|
37
|
-
id: ulid(),
|
|
38
|
-
sessionId,
|
|
39
|
-
timestamp: env.at,
|
|
40
|
-
source: 'client-runtime',
|
|
41
|
-
category: 'runtime',
|
|
42
|
-
severity: 'error',
|
|
43
|
-
context: { sessionId, ...tab },
|
|
44
|
-
fingerprint: fingerprint({
|
|
45
|
-
kind: 'runtime',
|
|
46
|
-
identity: `unhandledrejection:${env.reason.slice(0, 80)}`,
|
|
47
|
-
}),
|
|
48
|
-
title: 'Unhandled promise rejection',
|
|
49
|
-
message: env.reason,
|
|
50
|
-
raw: env,
|
|
51
|
-
};
|
|
52
|
-
case 'console': {
|
|
53
|
-
const severity = env.level === 'error' ? 'error' : env.level === 'warn' ? 'warning' : 'info';
|
|
54
|
-
const text = env.args.map(stringify).join(' ');
|
|
55
|
-
return {
|
|
56
|
-
id: ulid(),
|
|
57
|
-
sessionId,
|
|
58
|
-
timestamp: env.at,
|
|
59
|
-
source: 'client-runtime',
|
|
60
|
-
category: 'runtime',
|
|
61
|
-
severity,
|
|
62
|
-
context: { sessionId, ...tab },
|
|
63
|
-
fingerprint: fingerprint({
|
|
64
|
-
kind: 'runtime',
|
|
65
|
-
identity: `console:${env.level}:${text.slice(0, 80)}`,
|
|
66
|
-
}),
|
|
67
|
-
title: `console.${env.level}`,
|
|
68
|
-
message: text,
|
|
69
|
-
raw: env,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
case 'fetch': {
|
|
73
|
-
const failed = env.error !== undefined || (env.status !== undefined && env.status >= 400);
|
|
74
|
-
return {
|
|
75
|
-
id: ulid(),
|
|
76
|
-
sessionId,
|
|
77
|
-
timestamp: env.at,
|
|
78
|
-
source: 'client-runtime',
|
|
79
|
-
category: 'network',
|
|
80
|
-
severity: failed ? 'error' : 'info',
|
|
81
|
-
context: { sessionId, ...tab },
|
|
82
|
-
fingerprint: fingerprint({
|
|
83
|
-
kind: 'network',
|
|
84
|
-
identity: `${env.method}:${normaliseUrl(env.url)}:${env.status ?? 'err'}`,
|
|
85
|
-
}),
|
|
86
|
-
title: `${env.method} ${env.url}${env.status !== undefined ? ` → ${env.status}` : ''}`,
|
|
87
|
-
message: env.error,
|
|
88
|
-
relatedUrls: [env.url],
|
|
89
|
-
raw: env,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
case 'route':
|
|
93
|
-
return {
|
|
94
|
-
id: ulid(),
|
|
95
|
-
sessionId,
|
|
96
|
-
timestamp: env.at,
|
|
97
|
-
source: 'client-runtime',
|
|
98
|
-
category: 'runtime',
|
|
99
|
-
severity: 'info',
|
|
100
|
-
context: { sessionId, url: env.url, ...tab },
|
|
101
|
-
fingerprint: fingerprint({ kind: 'runtime', identity: `route:${env.url}` }),
|
|
102
|
-
title: `Route → ${env.url}`,
|
|
103
|
-
raw: env,
|
|
104
|
-
};
|
|
105
|
-
case 'ready':
|
|
106
|
-
return {
|
|
107
|
-
id: ulid(),
|
|
108
|
-
sessionId,
|
|
109
|
-
timestamp: env.at,
|
|
110
|
-
source: 'client-runtime',
|
|
111
|
-
category: 'runtime',
|
|
112
|
-
severity: 'info',
|
|
113
|
-
context: { sessionId, url: env.url, ...tab },
|
|
114
|
-
fingerprint: fingerprint({ kind: 'runtime', identity: 'client-ready' }),
|
|
115
|
-
title: 'Client runtime ready',
|
|
116
|
-
message: env.userAgent,
|
|
117
|
-
raw: env,
|
|
118
|
-
};
|
|
119
|
-
case 'publish':
|
|
120
|
-
// Pre-formed publish envelope from in-page instrumentation. Trust
|
|
121
|
-
// its source/category (the producer is our own injected code); the
|
|
122
|
-
// reducers gate on those, and the context carries the flow id so
|
|
123
|
-
// React renders correlate with the originating user action.
|
|
124
|
-
return {
|
|
125
|
-
id: ulid(),
|
|
126
|
-
sessionId,
|
|
127
|
-
timestamp: env.at,
|
|
128
|
-
source: env.source,
|
|
129
|
-
category: env.category,
|
|
130
|
-
severity: (env.severity ?? 'info'),
|
|
131
|
-
context: { sessionId, ...tab, ...(env.context ?? {}) },
|
|
132
|
-
fingerprint: env.fingerprint ?? fingerprint({ kind: 'publish', identity: env.title }),
|
|
133
|
-
title: env.title,
|
|
134
|
-
message: env.message,
|
|
135
|
-
raw: env.raw,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// --- helpers ---
|
|
140
|
-
function normaliseStackTopFrame(stack) {
|
|
141
|
-
if (!stack)
|
|
142
|
-
return 'unknown';
|
|
143
|
-
const lines = stack.split('\n').slice(0, 2);
|
|
144
|
-
for (const line of lines) {
|
|
145
|
-
const m = line.match(/at\s+(\S+)/);
|
|
146
|
-
if (m && m[1])
|
|
147
|
-
return m[1];
|
|
148
|
-
}
|
|
149
|
-
return 'unknown';
|
|
150
|
-
}
|
|
151
|
-
function normaliseUrl(url) {
|
|
152
|
-
try {
|
|
153
|
-
const u = new URL(url, 'http://x');
|
|
154
|
-
return `${u.pathname}`;
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return url;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function stringify(v) {
|
|
161
|
-
if (typeof v === 'string')
|
|
162
|
-
return v;
|
|
163
|
-
try {
|
|
164
|
-
return JSON.stringify(v);
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
return String(v);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
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");
|
package/lib/redact.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction + bounding — re-exported from `@lensmcp/core/redact`.
|
|
3
|
+
*
|
|
4
|
+
* The implementation used to live HERE, applied in `envelopeToEvent` (the one
|
|
5
|
+
* function every browser envelope crosses). That worked, and it covered only
|
|
6
|
+
* the browser: the cluster GATEWAY publishes its own events straight onto the
|
|
7
|
+
* same bus without crossing that function, and was found writing
|
|
8
|
+
* `?access_token=sk_live_…` verbatim into `title`, `raw.url` and
|
|
9
|
+
* `raw.steps[].url` of a real `.lensmcp/events.jsonl`.
|
|
10
|
+
*
|
|
11
|
+
* Fixing that needed the same redactor in `@lensmcp/cluster`, which sits at
|
|
12
|
+
* `layer:install` and cannot sensibly depend on this WebSocket-server package.
|
|
13
|
+
* So the module moved DOWN to `layer:core` — reachable by both — and this file
|
|
14
|
+
* stays as the re-export so nothing in the bridge (or any consumer of
|
|
15
|
+
* `@lensmcp/bridge`'s public API) has to change.
|
|
16
|
+
*
|
|
17
|
+
* There is exactly ONE implementation. Do not add logic here; edit
|
|
18
|
+
* `libs/core/src/lib/redact.ts`.
|
|
19
|
+
*/
|
|
20
|
+
export { clip, isSecretParam, MAX_MESSAGE_CHARS, MAX_TITLE_CHARS, MAX_URL_CHARS, redactDeep, redactText, redactUrl, redactUrlParams, REDACTED, safeText, safeTitle, } from '@lensmcp/core/redact';
|
package/lib/redact.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";export{clip,isSecretParam,MAX_MESSAGE_CHARS,MAX_TITLE_CHARS,MAX_URL_CHARS,redactDeep,redactText,redactUrl,redactUrlParams,REDACTED,safeText,safeTitle}from"@lensmcp/core/redact";
|
package/lib/server.d.ts
CHANGED
|
@@ -53,4 +53,3 @@ export declare function handleClientEnvelope(raw: string, publish: (event: BaseE
|
|
|
53
53
|
export declare function createBridgeServer(options?: BridgeServerOptions): Promise<BridgeServer>;
|
|
54
54
|
/** Allocate an ephemeral free port (used when `port: 0` is requested). */
|
|
55
55
|
export declare function findFreePort(): Promise<number>;
|
|
56
|
-
//# sourceMappingURL=server.d.ts.map
|
package/lib/server.js
CHANGED
|
@@ -1,109 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { WebSocketServer } from 'ws';
|
|
3
|
-
import { DEFAULT_WS_HOST, DEFAULT_WS_PORT } from './constants.js';
|
|
4
|
-
import { resolveEnvSink } from './env-sink.js';
|
|
5
|
-
import { envelopeToEvent } from './event-mappers.js';
|
|
6
|
-
/**
|
|
7
|
-
* Build a `publish` function honouring the bus → sink → env-sink → console
|
|
8
|
-
* precedence. Mirrors the logic the Vite plugin used to inline, lifted here
|
|
9
|
-
* so the standalone sidecar and the plugin share one path.
|
|
10
|
-
*/
|
|
11
|
-
export function makePublisher(deps) {
|
|
12
|
-
let resolvedSink;
|
|
13
|
-
return (event) => {
|
|
14
|
-
if (deps.bus) {
|
|
15
|
-
deps.bus.publish(event);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
if (deps.sink) {
|
|
19
|
-
deps.sink(event);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
// Resolve the env sink once, lazily — matches the Nest sink contract.
|
|
23
|
-
if (resolvedSink === undefined)
|
|
24
|
-
resolvedSink = resolveEnvSink();
|
|
25
|
-
if (resolvedSink) {
|
|
26
|
-
resolvedSink(event);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
// Standalone fallback so the bridge is debuggable without any transport.
|
|
30
|
-
if (event.severity === 'error' || event.severity === 'fatal') {
|
|
31
|
-
console.debug(`[lensmcp-bridge] ${event.severity}: ${event.title}`);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Parse one raw WebSocket message from the injected client runtime and
|
|
37
|
-
* forward the resulting event via `publish`. Exported so it can be
|
|
38
|
-
* exercised over a real WebSocket without booting any bundler.
|
|
39
|
-
*/
|
|
40
|
-
export function handleClientEnvelope(raw, publish, sessionId) {
|
|
41
|
-
let env;
|
|
42
|
-
try {
|
|
43
|
-
env = JSON.parse(raw);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return; // ignore malformed
|
|
47
|
-
}
|
|
48
|
-
if (!env || typeof env !== 'object' || !('kind' in env))
|
|
49
|
-
return;
|
|
50
|
-
publish(envelopeToEvent(env, sessionId));
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Stand up the browser-event bridge: a WebSocket server the injected
|
|
54
|
-
* `@lensmcp/client-runtime` connects to, forwarding every envelope onto
|
|
55
|
-
* the event bus (in-process or cross-process). Build-agnostic — the Vite
|
|
56
|
-
* plugin uses it on an ephemeral port; the standalone sidecar uses it on a
|
|
57
|
-
* fixed port for webpack / Next.js / no-build hosts.
|
|
58
|
-
*
|
|
59
|
-
* Resolves once the server is listening, with the actual bound `port`.
|
|
60
|
-
*/
|
|
61
|
-
export async function createBridgeServer(options = {}) {
|
|
62
|
-
const host = options.host ?? DEFAULT_WS_HOST;
|
|
63
|
-
const requestedPort = options.port ?? DEFAULT_WS_PORT;
|
|
64
|
-
const port = requestedPort === 0 ? await findFreePort() : requestedPort;
|
|
65
|
-
const publish = makePublisher(options);
|
|
66
|
-
const wss = new WebSocketServer({ host, port });
|
|
67
|
-
wss.on('connection', (socket) => {
|
|
68
|
-
socket.on('message', (raw) => {
|
|
69
|
-
handleClientEnvelope(raw.toString(), publish, options.sessionId);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
await new Promise((resolve, reject) => {
|
|
73
|
-
const onError = (err) => {
|
|
74
|
-
wss.off('listening', onListening);
|
|
75
|
-
reject(err);
|
|
76
|
-
};
|
|
77
|
-
const onListening = () => {
|
|
78
|
-
wss.off('error', onError);
|
|
79
|
-
resolve();
|
|
80
|
-
};
|
|
81
|
-
wss.once('error', onError);
|
|
82
|
-
wss.once('listening', onListening);
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
port,
|
|
86
|
-
host,
|
|
87
|
-
close: () => new Promise((resolve) => {
|
|
88
|
-
wss.close(() => resolve());
|
|
89
|
-
}),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/** Allocate an ephemeral free port (used when `port: 0` is requested). */
|
|
93
|
-
export function findFreePort() {
|
|
94
|
-
return new Promise((resolve, reject) => {
|
|
95
|
-
const s = createNetServer();
|
|
96
|
-
s.unref();
|
|
97
|
-
s.on('error', reject);
|
|
98
|
-
s.listen(0, () => {
|
|
99
|
-
const addr = s.address();
|
|
100
|
-
if (!addr) {
|
|
101
|
-
s.close();
|
|
102
|
-
reject(new Error('Could not allocate free port'));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const freePort = addr.port;
|
|
106
|
-
s.close(() => resolve(freePort));
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
}
|
|
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");
|
package/lib/types.d.ts
CHANGED
|
@@ -8,18 +8,52 @@
|
|
|
8
8
|
* no-build) shares one definition. The Vite plugin re-exports it for
|
|
9
9
|
* backwards compatibility.
|
|
10
10
|
*/
|
|
11
|
-
export type ClientEnvelope = (
|
|
11
|
+
export type ClientEnvelope = (
|
|
12
|
+
/**
|
|
13
|
+
* An uncaught JS error (`window.onerror`). `message` is OPTIONAL because it
|
|
14
|
+
* genuinely can be absent: a synthesised or cross-origin `ErrorEvent`
|
|
15
|
+
* carries none, and 3 of the 7 real browser errors captured on foodguard
|
|
16
|
+
* (2026-07-30) had no message at all. `name` is the constructor name of the
|
|
17
|
+
* thrown value, which survives when a cross-origin policy strips the rest.
|
|
18
|
+
*/
|
|
19
|
+
{
|
|
12
20
|
kind: 'error';
|
|
13
21
|
at: number;
|
|
14
|
-
message
|
|
22
|
+
message?: string;
|
|
23
|
+
name?: string;
|
|
15
24
|
stack?: string;
|
|
16
25
|
source?: string;
|
|
17
26
|
lineno?: number;
|
|
18
27
|
colno?: number;
|
|
19
|
-
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A SUBRESOURCE that failed to load — `<img>`, `<script>`, `<link>`,
|
|
31
|
+
* `<video>`. The browser delivers these through the SAME `error` listener as
|
|
32
|
+
* an uncaught exception, but as a plain `Event`, not an `ErrorEvent`: no
|
|
33
|
+
* message, no filename, no line. Its only identity is the target element and
|
|
34
|
+
* its `src`/`href`, which the tap previously never read — so every one of
|
|
35
|
+
* them serialised to a bare `{ kind: 'error', at, tabId }` and arrived
|
|
36
|
+
* completely anonymous. Splitting the kind is what makes that identity
|
|
37
|
+
* representable.
|
|
38
|
+
*/
|
|
39
|
+
| {
|
|
40
|
+
kind: 'resource-error';
|
|
41
|
+
at: number;
|
|
42
|
+
tagName: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* An unhandled promise rejection. The reason is decomposed rather than
|
|
47
|
+
* pre-stringified: a rejected non-Error object (`{ status: 401 }`, a
|
|
48
|
+
* `Response`) used to be flattened by `String(reason)` into the useless
|
|
49
|
+
* `"[object Object]"`.
|
|
50
|
+
*/
|
|
51
|
+
| {
|
|
20
52
|
kind: 'unhandledrejection';
|
|
21
53
|
at: number;
|
|
22
54
|
reason: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
stack?: string;
|
|
23
57
|
} | {
|
|
24
58
|
kind: 'console';
|
|
25
59
|
at: number;
|
|
@@ -31,6 +65,7 @@ export type ClientEnvelope = ({
|
|
|
31
65
|
method: string;
|
|
32
66
|
url: string;
|
|
33
67
|
status?: number;
|
|
68
|
+
statusText?: string;
|
|
34
69
|
durationMs?: number;
|
|
35
70
|
ok?: boolean;
|
|
36
71
|
error?: string;
|
|
@@ -61,4 +96,3 @@ export type ClientEnvelope = ({
|
|
|
61
96
|
};
|
|
62
97
|
/** A cross-process event sink: takes one normalised event and forwards it onward. */
|
|
63
98
|
export type EnvSink = (event: import('@lensmcp/protocol-types').BaseEvent) => void;
|
|
64
|
-
//# sourceMappingURL=types.d.ts.map
|
package/lib/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
"use strict";export{};
|
package/main.d.ts
CHANGED
package/main.js
CHANGED
|
@@ -1,47 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Standalone bridge sidecar — the build-agnostic entry.
|
|
3
|
-
*
|
|
4
|
-
* Run directly (`node bridge.js`) or via `lensmcp bridge`. Stands up the
|
|
5
|
-
* WebSocket the injected `@lensmcp/client-runtime` connects to, on a FIXED
|
|
6
|
-
* port (so a no-build / webpack / Next.js host can point at a known URL),
|
|
7
|
-
* and forwards every browser envelope onto the cross-process event sink
|
|
8
|
-
* the MCP server tails.
|
|
9
|
-
*
|
|
10
|
-
* Env:
|
|
11
|
-
* LENSMCP_WS_HOST bind host (default 127.0.0.1)
|
|
12
|
-
* LENSMCP_WS_PORT bind port (default 5747; `0` = ephemeral)
|
|
13
|
-
* LENSMCP_SESSION_ID session id stamped onto every event (optional)
|
|
14
|
-
* LENSMCP_EVENT_FILE JSONL sink the MCP server ingests (or LENSMCP_UDS /
|
|
15
|
-
* LENSMCP_IPC_SOCKET) — resolved by the env sink.
|
|
16
|
-
*/
|
|
17
|
-
import { DEFAULT_WS_HOST, DEFAULT_WS_PORT, ENV_SESSION_ID, ENV_WS_HOST, ENV_WS_PORT } from './lib/constants.js';
|
|
18
|
-
import { createBridgeServer } from './lib/server.js';
|
|
19
|
-
async function main() {
|
|
20
|
-
const host = process.env[ENV_WS_HOST] ?? DEFAULT_WS_HOST;
|
|
21
|
-
const portRaw = process.env[ENV_WS_PORT];
|
|
22
|
-
const port = portRaw !== undefined && portRaw !== '' ? Number(portRaw) : DEFAULT_WS_PORT;
|
|
23
|
-
if (!Number.isFinite(port) || port < 0) {
|
|
24
|
-
console.error(`[lensmcp-bridge] invalid ${ENV_WS_PORT}=${portRaw}`);
|
|
25
|
-
process.exit(2);
|
|
26
|
-
}
|
|
27
|
-
const sessionId = process.env[ENV_SESSION_ID];
|
|
28
|
-
let server;
|
|
29
|
-
try {
|
|
30
|
-
server = await createBridgeServer({ host, port, sessionId });
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
console.error(`[lensmcp-bridge] failed to listen on ws://${host}:${port} — ${err.message}`);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
console.log(`[lensmcp-bridge] listening on ws://${server.host}:${server.port}`);
|
|
37
|
-
let closing = false;
|
|
38
|
-
const shutdown = () => {
|
|
39
|
-
if (closing)
|
|
40
|
-
return;
|
|
41
|
-
closing = true;
|
|
42
|
-
void server.close().then(() => process.exit(0));
|
|
43
|
-
};
|
|
44
|
-
process.on('SIGINT', shutdown);
|
|
45
|
-
process.on('SIGTERM', shutdown);
|
|
46
|
-
}
|
|
47
|
-
void main();
|
|
1
|
+
"use strict";var S=Object.defineProperty;var n=(s,e)=>S(s,"name",{value:e,configurable:!0});var d=Object.defineProperty,i=n((s,e)=>d(s,"name",{value:e,configurable:!0}),"n");import{DEFAULT_WS_HOST as _,DEFAULT_WS_PORT as g,ENV_SESSION_ID as v,ENV_WS_HOST as u,ENV_WS_PORT as a}from"./lib/constants.js";import{createBridgeServer as T}from"./lib/server.js";async function l(){const s=process.env[u]??_,e=process.env[a],o=e!==void 0&&e!==""?Number(e):g;(!Number.isFinite(o)||o<0)&&(console.error(`[lensmcp-bridge] invalid ${a}=${e}`),process.exit(2));const p=process.env[v];let r;try{r=await T({host:s,port:o,sessionId:p})}catch(m){console.error(`[lensmcp-bridge] failed to listen on ws://${s}:${o} \u2014 ${m.message}`),process.exit(1)}console.log(`[lensmcp-bridge] listening on ws://${r.host}:${r.port}`);let t=!1;const c=i(()=>{t||(t=!0,r.close().then(()=>process.exit(0)))},"shutdown");process.on("SIGINT",c),process.on("SIGTERM",c)}n(l,"g"),i(l,"main"),l();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/bridge",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.6",
|
|
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.6",
|
|
23
|
+
"@lensmcp/protocol-types": "1.18.6",
|
|
24
24
|
"tslib": "^2.3.0",
|
|
25
25
|
"ws": "^8.21.0"
|
|
26
26
|
},
|
package/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,GAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAClF,OAAO,EACL,eAAe,EACf,eAAe,EACf,WAAW,EACX,WAAW,EACX,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
|
package/lib/constants.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/lib/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C,4FAA4F;AAC5F,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,oDAAoD;AACpD,eAAO,MAAM,WAAW,oBAAoB,CAAC;AAC7C,oFAAoF;AACpF,eAAO,MAAM,WAAW,oBAAoB,CAAC;AAC7C,yEAAyE;AACzE,eAAO,MAAM,cAAc,uBAAuB,CAAC"}
|
package/lib/env-sink.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"env-sink.d.ts","sourceRoot":"","sources":["../../src/lib/env-sink.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAwF1C,wBAAgB,cAAc,IAAI,OAAO,GAAG,IAAI,CA6C/C;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgDhD"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"event-mappers.d.ts","sourceRoot":"","sources":["../../src/lib/event-mappers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIjD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,SAAS,SAAyB,GAAG,SAAS,CAuIlG"}
|
package/lib/server.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/lib/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAI9C,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAsB1E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,EACnC,SAAS,CAAC,EAAE,MAAM,GACjB,IAAI,CASN;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmCjG;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAgB9C"}
|
package/lib/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GAAG,CACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAChH;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,EAAE,CAAA;CAAE,GACpG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9H;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAK7D;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CACJ,GAAG;IACF,0GAA0G;IAC1G,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,OAAO,yBAAyB,EAAE,SAAS,KAAK,IAAI,CAAC"}
|
package/main.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
|