@lensmcp/bridge 1.23.13 → 1.24.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/README.md CHANGED
@@ -34,6 +34,36 @@ lensmcp bridge
34
34
  | `LENSMCP_WS_PORT` | `5747` | bind port (`0` = ephemeral) |
35
35
  | `LENSMCP_SESSION_ID` | — | session id stamped on each event |
36
36
  | `LENSMCP_EVENT_FILE` | — | JSONL sink the MCP server ingests |
37
+ | `LENSMCP_BRIDGE_ALLOWED_ORIGINS` | — | browser origins admitted beyond loopback, comma-separated (`https://app.test:8443`, `app.test`, `.app.test`, `null`, `*`) |
38
+ | `LENSMCP_BRIDGE_TOKEN` | — | when set, every upgrade must carry it as `?token=` |
39
+
40
+ On SIGTERM or SIGINT it stops accepting, drops every connected page (each
41
+ tab's `gone` is published), releases the port, settles the event transport,
42
+ and exits 0 — open tabs do not hold it. Anything that still hangs is cut off
43
+ after 3 s (exit 1); a second signal exits at once.
44
+
45
+ ### Who may connect
46
+
47
+ Browsers apply no same-origin policy to WebSockets, so every page the
48
+ developer visits can reach the bridge's port. Each upgrade is therefore
49
+ checked before a socket exists:
50
+
51
+ - **Origin.** A browser always sends `Origin` on an upgrade. Pages from
52
+ loopback origins (`localhost`, `*.localhost`, `127.0.0.0/8`, `[::1]`) are
53
+ admitted, plus `LENSMCP_BRIDGE_ALLOWED_ORIGINS`; anything else is refused
54
+ with a 403 (and one console line naming the origin). An opaque `Origin: null`
55
+ is refused. No `Origin` at all is a non-browser client (a Node producer, a
56
+ test) and is admitted — a page cannot produce that shape.
57
+ - **Token** (optional). With `LENSMCP_BRIDGE_TOKEN` set, an upgrade without
58
+ it is refused with a 401. Give the page the same value via
59
+ `window.__LENSMCP_WS_TOKEN__` or `<meta name="lensmcp-ws-token">`.
60
+ - **Bounds.** At most 64 sockets; each socket gets a per-connection budget
61
+ (4096 envelopes, refilling at 512/s — every envelope inside a batch
62
+ counts). Envelopes past it are dropped and the next admitted one is
63
+ preceded by a `console.warn` saying how many.
64
+ - **Schema.** Every envelope is checked field by field against the
65
+ `BaseEventSchema` fields it becomes, and the mapped event must parse as a
66
+ `BaseEvent`; anything else is dropped.
37
67
 
38
68
  Point the browser at it with `@lensmcp/client-runtime/auto` (zero build):
39
69
 
@@ -46,10 +76,16 @@ import '@lensmcp/client-runtime/auto';
46
76
  ```ts
47
77
  import { createBridgeServer } from '@lensmcp/bridge';
48
78
 
49
- const bridge = await createBridgeServer({ port: 0, bus });
79
+ const bridge = await createBridgeServer({ port: 0, bus, token, allowedOrigins: ['https://app.test'] });
50
80
  console.log(bridge.port);
51
- await bridge.close();
81
+ bridge.learnOrigin('https://app.local'); // an origin the host has seen itself serve
82
+ await bridge.close(); // drops connected pages, releases the port — the process keeps running
52
83
  ```
53
84
 
85
+ The Vite plugin embeds it this way: it mints a token per dev-server start,
86
+ bakes it into the runtime it serves, and teaches the bridge every origin it
87
+ serves that runtime to (so a page behind the LensMCP gateway is admitted
88
+ without configuration).
89
+
54
90
  The bridge is **strictly passive**: it never mutates app state, never
55
91
  changes scheduling, and never throws into the host.
package/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- export { createBridgeServer, makePublisher, handleClientEnvelope, findFreePort, type BridgeServer, type BridgeServerOptions, type BridgeDeps, } from './lib/server.js';
1
+ export { createBridgeServer, makePublisher, handleClientEnvelope, findFreePort, CONNECTION_THROTTLE_NOTICE, type BridgeServer, type BridgeServerOptions, type BridgeDeps, type BridgeRefusal, type ConnectionRateLimit, type EnvelopeIngress, } from './lib/server.js';
2
+ export { createOriginPolicy, parseAllowedOrigins, isLoopbackHostname, MAX_LEARNED_ORIGINS, type OriginPolicy, } from './lib/origin-policy.js';
3
+ export { conformsToEnvelopeContract } from './lib/envelope-contract.js';
2
4
  export { envelopeToEvent } from './lib/event-mappers.js';
3
5
  export { redactUrl, redactText, redactDeep, isSecretParam, clip, safeText, safeTitle, REDACTED, MAX_TITLE_CHARS, MAX_MESSAGE_CHARS, MAX_URL_CHARS, } from './lib/redact.js';
4
- export { resolveEnvSink, resetEnvSinkCache, udsEnvSink } from './lib/env-sink.js';
5
- export { DEFAULT_WS_HOST, DEFAULT_WS_PORT, ENV_WS_HOST, ENV_WS_PORT, ENV_SESSION_ID, } from './lib/constants.js';
6
+ export { resolveEnvSink, resetEnvSinkCache, closeEnvSink, udsEnvSink } from './lib/env-sink.js';
7
+ export { DEFAULT_WS_HOST, DEFAULT_WS_PORT, ENV_WS_HOST, ENV_WS_PORT, ENV_SESSION_ID, ENV_BRIDGE_ALLOWED_ORIGINS, ENV_BRIDGE_TOKEN, BRIDGE_TOKEN_PARAM, MAX_BRIDGE_CONNECTIONS, CONNECTION_BURST, CONNECTION_PER_SEC, SHUTDOWN_GRACE_MS, ENV_SINK_CLOSE_GRACE_MS, } from './lib/constants.js';
6
8
  export type { ClientEnvelope, EnvSink } from './lib/types.js';
package/index.js CHANGED
@@ -1 +1 @@
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";
1
+ "use strict";export{createBridgeServer,makePublisher,handleClientEnvelope,findFreePort,CONNECTION_THROTTLE_NOTICE}from"./lib/server.js";export{createOriginPolicy,parseAllowedOrigins,isLoopbackHostname,MAX_LEARNED_ORIGINS}from"./lib/origin-policy.js";export{conformsToEnvelopeContract}from"./lib/envelope-contract.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,closeEnvSink,udsEnvSink}from"./lib/env-sink.js";export{DEFAULT_WS_HOST,DEFAULT_WS_PORT,ENV_WS_HOST,ENV_WS_PORT,ENV_SESSION_ID,ENV_BRIDGE_ALLOWED_ORIGINS,ENV_BRIDGE_TOKEN,BRIDGE_TOKEN_PARAM,MAX_BRIDGE_CONNECTIONS,CONNECTION_BURST,CONNECTION_PER_SEC,SHUTDOWN_GRACE_MS,ENV_SINK_CLOSE_GRACE_MS}from"./lib/constants.js";
@@ -8,6 +8,10 @@
8
8
  * on a FIXED, env-overridable port — so the browser runtime can find it
9
9
  * without a build step baking the URL in.
10
10
  *
11
+ * Who may connect is decided at the upgrade (`origin-policy.ts`, `server.ts`): browsers always send
12
+ * `Origin`, so a page is admitted only from an allowed origin, and a bridge configured with a token
13
+ * refuses any upgrade that does not carry it.
14
+ *
11
15
  * `DEFAULT_WS_PORT` must agree between the standalone bridge runner
12
16
  * (`@lensmcp/bridge`) and the no-build client entry
13
17
  * (`@lensmcp/client-runtime/auto`), which lives in a separate
@@ -22,12 +26,41 @@ export declare const ENV_WS_HOST = "LENSMCP_WS_HOST";
22
26
  export declare const ENV_WS_PORT = "LENSMCP_WS_PORT";
23
27
  /** Env var: session id stamped onto every event the bridge publishes. */
24
28
  export declare const ENV_SESSION_ID = "LENSMCP_SESSION_ID";
29
+ /**
30
+ * Env var: browser origins the standalone bridge admits BEYOND loopback, comma-separated. Each entry is
31
+ * a full origin (`https://app.test:8443`), a hostname (`app.test`), a subdomain wildcard
32
+ * (`.app.test`), `null` (opaque origins), or `*` (any). See `origin-policy.ts`.
33
+ */
34
+ export declare const ENV_BRIDGE_ALLOWED_ORIGINS = "LENSMCP_BRIDGE_ALLOWED_ORIGINS";
35
+ /** Env var: when set, the standalone bridge refuses any upgrade that does not carry it as `?token=`. */
36
+ export declare const ENV_BRIDGE_TOKEN = "LENSMCP_BRIDGE_TOKEN";
37
+ /** The query parameter an upgrade carries its bridge token in (`ws://host:port/?token=…`). */
38
+ export declare const BRIDGE_TOKEN_PARAM = "token";
39
+ /**
40
+ * Most sockets one bridge holds open at once.
41
+ *
42
+ * A legitimate client is one socket per open TAB, so this is far above a developer's tab count and
43
+ * still a hard ceiling on what one hostile client can hold (issues/closed/codex-security-32).
44
+ */
45
+ export declare const MAX_BRIDGE_CONNECTIONS = 64;
46
+ /**
47
+ * Per-CONNECTION envelope budget: a token bucket of {@link CONNECTION_BURST} envelopes refilling at
48
+ * {@link CONNECTION_PER_SEC}/s. Every envelope counts, including each one inside a batch.
49
+ *
50
+ * This is NOT the per-fingerprint flood guard downstream — that one groups repetition and is keyed on
51
+ * a fingerprint the envelope itself chooses, so unique fingerprints step around it. This bounds what
52
+ * one socket can push, whatever it calls its events. The burst covers a full client flush (8 frames ×
53
+ * 200 events plus boundaries) twice over; the sustained rate is above what the in-page producers emit
54
+ * after their own caps (25 renders per commit, 15 commits/s, 5/s per fingerprint).
55
+ */
56
+ export declare const CONNECTION_BURST = 4096;
57
+ export declare const CONNECTION_PER_SEC = 512;
25
58
  /**
26
59
  * Largest single WebSocket frame the bridge will accept, in bytes.
27
60
  *
28
61
  * `ws` defaults to 100 MiB per message, and `handleClientEnvelope` JSON-parses whatever arrives —
29
62
  * so the default made one frame from any page the developer visits an out-of-memory lever
30
- * (issues/codex-security/32). Browser envelopes are small; the `raw` payload is separately clamped
63
+ * (issues/closed/codex-security-32). Browser envelopes are small; the `raw` payload is separately clamped
31
64
  * to 32 KiB downstream, so 1 MiB is generous for a legitimate producer and useless as a weapon.
32
65
  */
33
66
  export declare const MAX_FRAME_BYTES: number;
@@ -35,7 +68,20 @@ export declare const MAX_FRAME_BYTES: number;
35
68
  * Largest number of inner envelopes one `batch` frame may carry.
36
69
  *
37
70
  * Batching exists to coalesce a render burst, not to admit an unbounded stream in a single message
38
- * (issues/codex-security/19, 21, 32). The client batches per animation frame, so this is far above
71
+ * (issues/closed/codex-security-19, 21, 32). The client batches per animation frame, so this is far above
39
72
  * anything it produces.
40
73
  */
41
74
  export declare const MAX_BATCH_EVENTS = 1024;
75
+ /**
76
+ * How long the standalone bridge may take to shut down after SIGTERM/SIGINT before it exits anyway
77
+ * (issues/closed/shared-default-02). A clean shutdown — every page socket dropped, the port released,
78
+ * the bus transport settled — takes milliseconds; this is the ceiling for one that hangs, and what a
79
+ * supervisor such as `agent-dev` waits on at most.
80
+ */
81
+ export declare const SHUTDOWN_GRACE_MS = 3000;
82
+ /**
83
+ * How long settling the bus transport may take on shutdown — handing a connecting UDS socket the events
84
+ * still queued for it. Below {@link SHUTDOWN_GRACE_MS}, so a bus peer that stopped reading costs the
85
+ * events it would not take, not a forced exit.
86
+ */
87
+ export declare const ENV_SINK_CLOSE_GRACE_MS = 1000;
package/lib/constants.js CHANGED
@@ -1 +1 @@
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",MAX_FRAME_BYTES=1048576,MAX_BATCH_EVENTS=1024;
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",ENV_BRIDGE_ALLOWED_ORIGINS="LENSMCP_BRIDGE_ALLOWED_ORIGINS",ENV_BRIDGE_TOKEN="LENSMCP_BRIDGE_TOKEN",BRIDGE_TOKEN_PARAM="token",MAX_BRIDGE_CONNECTIONS=64,CONNECTION_BURST=4096,CONNECTION_PER_SEC=512,MAX_FRAME_BYTES=1048576,MAX_BATCH_EVENTS=1024,SHUTDOWN_GRACE_MS=3e3,ENV_SINK_CLOSE_GRACE_MS=1e3;
package/lib/env-sink.d.ts CHANGED
@@ -2,5 +2,13 @@ import type { EnvSink } from './types.js';
2
2
  export declare function resolveEnvSink(): EnvSink | null;
3
3
  /** Test/embedding hook: drop the memoised sink so the next resolve re-reads env. */
4
4
  export declare function resetEnvSinkCache(): void;
5
+ /**
6
+ * Settle the resolved env sink before the process exits (issues/closed/shared-default-02): the UDS
7
+ * transport may still be connecting with events queued — the standalone bridge's last ones are the
8
+ * `gone` it publishes for every tab it drops on shutdown — and `process.exit` discards whatever user
9
+ * space still holds. The file transport appends synchronously and has nothing to settle. Bounded by
10
+ * `graceMs`, so a bus peer that stopped reading cannot hold the exit. The next resolve re-reads env.
11
+ */
12
+ export declare function closeEnvSink(graceMs?: number): Promise<void>;
5
13
  /** Reconnecting NDJSON client over a Unix domain socket (see session/uds.ts). */
6
14
  export declare function udsEnvSink(path: string): EnvSink;
package/lib/env-sink.js CHANGED
@@ -1,3 +1,3 @@
1
- "use strict";var h=Object.defineProperty;var f=(e,t)=>h(e,"name",{value:t,configurable:!0});var k=Object.defineProperty,c=f((e,t)=>k(e,"name",{value:t,configurable:!0}),"i");import{connect as _}from"node:net";import{createSocket as M}from"node:dgram";import{appendFileSync as b,closeSync as A,openSync as C,renameSync as g,statSync as p,unlinkSync as F}from"node:fs";import{rotateBusFile as L}from"@lensmcp/core/bus-rotation";const E=(()=>{const e=Number(process.env.LENSMCP_EVENT_MAX_BYTES);return Number.isFinite(e)&&e>0?e:50*1024*1024})(),P=(()=>{const e=Number(process.env.LENSMCP_EVENT_MAX_FILES);return Number.isFinite(e)&&e>=1&&e<=20?Math.floor(e):5})(),w=1024*1024;let S=0;function m(e,t){if(S+=t,!(S<w)){S=0;try{if(p(e).size<=E)return}catch{return}L(e,{maxBytes:E,maxFiles:P,fs:{statSync:p,renameSync:g,unlinkSync:F,openSync:C,closeSync:A}})}}f(m,"L"),c(m,"rotateIfLarge");const a=(()=>{const e=process.env.LENSMCP_EVENT_MAX_EPS;if(e==="off")return 1/0;const t=Number(e);return e!==void 0&&e!==""&&Number.isFinite(t)?t<=0?1/0:t:5})(),d=Math.max(30,a===1/0?0:a*4),l=new Map;function v(e){if(a===1/0)return 0;const t=Date.now();let n=l.get(e);if(!n){if(l.size>=2048){for(const[r,s]of l)t-s.lastSeenAt>6e4&&l.delete(r);l.size>=2048&&l.clear()}n={tokens:d,lastRefillAt:t,suppressed:0,lastSeenAt:t},l.set(e,n)}if(n.lastSeenAt=t,n.tokens=Math.min(d,n.tokens+(t-n.lastRefillAt)/1e3*a),n.lastRefillAt=t,n.tokens>=1){n.tokens-=1;const r=n.suppressed;return n.suppressed=0,r}return n.suppressed+=1,-1}f(v,"P"),c(v,"floodAdmit");let i;export function resolveEnvSink(){if(i!==void 0)return i;const e=process.env.LENSMCP_EVENT_FILE;if(e)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
- `;m(e,u.length),b(e,u)}catch{}},"cachedEnvSink"),i;const t=process.env.LENSMCP_UDS;if(t)return i=udsEnvSink(t),i;const n=process.env.LENSMCP_IPC_SOCKET;if(n){const[r,s]=n.split(":"),u=Number(s);if(r&&Number.isFinite(u)){const o=M("udp4");return o.unref(),i=c(y=>{try{const N=Buffer.from(JSON.stringify(y));o.send(N,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(e){const t=[];let n=null,r=!1;const s=c(()=>{if(!(!n||!n.writable))for(;t.length>0;)n.write(t.shift())},"flush"),u=c(()=>{if(r||n)return;r=!0;const o=_(e);o.on("connect",()=>{r=!1,n=o,s()}),o.on("drain",s),o.on("error",()=>{r=!1,n=null}),o.on("close",()=>{n=null}),o.unref()},"open");return u(),o=>{for(t.push(JSON.stringify(o)+`
3
- `);t.length>1e4;)t.shift();if(n&&n.writable){s();return}if(n){try{n.destroy()}catch{}n=null}u()}}f(udsEnvSink,"udsEnvSink"),c(udsEnvSink,"udsEnvSink");
1
+ "use strict";var _=Object.defineProperty;var f=(t,n)=>_(t,"name",{value:n,configurable:!0});var b=Object.defineProperty,r=f((t,n)=>b(t,"name",{value:n,configurable:!0}),"r");import{connect as P}from"node:net";import{createSocket as A}from"node:dgram";import{appendFileSync as M,closeSync as T,openSync as F,renameSync as L,statSync as O,unlinkSync as x}from"node:fs";import{busCapsFromEnv as R,createBusRotator as I}from"@lensmcp/core/bus-rotation";import{ENV_SINK_CLOSE_GRACE_MS as B}from"./constants.js";const D=R(),d=(()=>{const t=process.env.LENSMCP_EVENT_MAX_EPS;if(t==="off")return 1/0;const n=Number(t);return t!==void 0&&t!==""&&Number.isFinite(n)?n<=0?1/0:n:5})(),y=Math.max(30,d===1/0?0:d*4),S=new Map;function k(t){if(d===1/0)return 0;const n=Date.now();let e=S.get(t);if(!e){if(S.size>=2048){for(const[s,o]of S)n-o.lastSeenAt>6e4&&S.delete(s);S.size>=2048&&S.clear()}e={tokens:y,lastRefillAt:n,suppressed:0,lastSeenAt:n},S.set(t,e)}if(e.lastSeenAt=n,e.tokens=Math.min(y,e.tokens+(n-e.lastRefillAt)/1e3*d),e.lastRefillAt=n,e.tokens>=1){e.tokens-=1;const s=e.suppressed;return e.suppressed=0,s}return e.suppressed+=1,-1}f(k,"B"),r(k,"floodAdmit");let u,m;export function resolveEnvSink(){if(u!==void 0)return u;const t=process.env.LENSMCP_EVENT_FILE;if(t){const s=I(t,{...D,fs:{statSync:O,renameSync:L,unlinkSync:x,openSync:F,closeSync:T}});return u=r(o=>{try{const c=k(o.fingerprint);if(c<0)return;const i=JSON.stringify(c>0?{...o,raw:{...o.raw??{},floodSuppressed:c}}:o)+`
2
+ `;s(i.length),M(t,i)}catch{}},"cachedEnvSink"),u}const n=process.env.LENSMCP_UDS;if(n){const s=v(n);return u=s.write,m=s.close,u}const e=process.env.LENSMCP_IPC_SOCKET;if(e){const[s,o]=e.split(":"),c=Number(o);if(s&&Number.isFinite(c)){const i=w(s,c);return u=i.write,m=i.close,u}}return u=null,null}f(resolveEnvSink,"resolveEnvSink"),r(resolveEnvSink,"resolveEnvSink");export function resetEnvSinkCache(){u=void 0,m=void 0}f(resetEnvSinkCache,"resetEnvSinkCache"),r(resetEnvSinkCache,"resetEnvSinkCache");export function closeEnvSink(t=B){const n=m;return resetEnvSinkCache(),n?n(t):Promise.resolve()}f(closeEnvSink,"closeEnvSink"),r(closeEnvSink,"closeEnvSink");export function udsEnvSink(t){return v(t).write}f(udsEnvSink,"udsEnvSink"),r(udsEnvSink,"udsEnvSink");function v(t){const n=[];let e=null,s=!1,o,c=!1;const i=r(()=>{if(!(!e||!e.writable))for(;n.length>0;)e.write(n.shift())},"flush"),a=r(()=>{if(s||e)return;s=!0;const l=P(t);l.on("connect",()=>{s=!1,e=l,i(),o?.(!0)}),l.on("drain",i),l.on("error",()=>{s=!1,e=null,o?.(!1)}),l.on("close",()=>{e=null}),l.unref()},"open");return a(),{write:r(l=>{if(!c){for(n.push(JSON.stringify(l)+`
3
+ `);n.length>1e4;)n.shift();if(e&&e.writable){i();return}if(e){try{e.destroy()}catch{}e=null}a()}},"write"),close:r(l=>new Promise(N=>{c=!0;let E=!1;const p=r(()=>{if(!E){E=!0,clearTimeout(g),o=void 0,n.length=0;try{e?.destroy()}catch{}e=null,N()}},"finish"),g=setTimeout(p,l),h=r(()=>{if(!e)return p();i(),e.end(p)},"endWhenFlushed");e?.writable?h():n.length===0?p():(o=r(C=>C?h():p(),"onConnectSettled"),a())}),"close")}}f(v,"y"),r(v,"openUdsSink");function w(t,n){const e=A("udp4");e.unref();let s=0,o;return{write:r(c=>{try{const i=Buffer.from(JSON.stringify(c));e.send(i,n,t,()=>{s-=1,s===0&&o?.()}),s+=1}catch{}},"write"),close:r(c=>new Promise(i=>{const a=r(()=>{clearTimeout(l),o=void 0;try{e.close()}catch{}i()},"finish"),l=setTimeout(a,c);s===0?a():o=a}),"close")}}f(w,"D"),r(w,"openUdpSink");
@@ -0,0 +1,5 @@
1
+ import { type BaseEvent } from '@lensmcp/protocol-types';
2
+ /** Does this (non-batch) envelope carry every field its kind needs, each in the shape the event needs? */
3
+ export declare function conformsToEnvelopeContract(envelope: object): boolean;
4
+ /** The mapped event as `BaseEventSchema` parses it, or undefined when it is not a `BaseEvent`. */
5
+ export declare function asBaseEvent(candidate: BaseEvent): BaseEvent | undefined;
@@ -0,0 +1 @@
1
+ "use strict";var g=Object.defineProperty;var a=(t,e)=>g(t,"name",{value:e,configurable:!0});var d=Object.defineProperty,s=a((t,e)=>d(t,"name",{value:e,configurable:!0}),"r");import{BaseEventSchema as f,TraceContextSchema as l}from"@lensmcp/protocol-types";const r=s(t=>e=>t.safeParse(e).success,"conforms"),n=s(t=>typeof t=="string","isString"),p=s((...t)=>e=>typeof e=="string"&&t.includes(e),"oneOf"),o=f.shape,m={at:r(o.timestamp),tabId:r(l.shape.tabId)},v={error:{},"resource-error":{tagName:n},unhandledrejection:{reason:n},console:{level:p("log","info","warn","error","debug"),args:Array.isArray},fetch:{method:n,url:n},route:{url:n},ready:{userAgent:n,url:n},hmr:{event:p("full-reload","update","invalidate","ws-disconnect","ws-connect")},gone:{},publish:{source:r(o.source),category:r(o.category),severity:r(o.severity.optional()),title:r(o.title),message:r(o.message),fingerprint:r(o.fingerprint.optional()),context:r(l.partial().optional())}},y=new Map(Object.entries(v));export function conformsToEnvelopeContract(t){const e=t,i=e.kind,c=typeof i=="string"?y.get(i):void 0;return c?u(e,m)&&u(e,c):!1}a(conformsToEnvelopeContract,"conformsToEnvelopeContract"),s(conformsToEnvelopeContract,"conformsToEnvelopeContract");export function asBaseEvent(t){const e=f.safeParse(t);return e.success?e.data:void 0}a(asBaseEvent,"asBaseEvent"),s(asBaseEvent,"asBaseEvent");function u(t,e){for(const[i,c]of Object.entries(e))if(!c(t[i]))return!1;return!0}a(u,"l"),s(u,"satisfies");
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Which browser origins may open the bridge WebSocket (issues/closed/codex-security-32, 41, 68).
3
+ *
4
+ * Browsers do not apply same-origin policy to WebSockets, so every page the developer visits can
5
+ * reach `ws://127.0.0.1:<port>` — but a browser ALWAYS sends `Origin` on the upgrade (a WebSocket
6
+ * request's mode is "websocket", and Fetch appends `Origin` to every such request). That header, which
7
+ * page script cannot set, is what tells the app being observed apart from any other tab.
8
+ *
9
+ * - no `Origin` → not a browser: a Node producer, a test, a smoke script. Admitted HERE — a page
10
+ * cannot produce this shape, and a local process can already read a token out of the served runtime
11
+ * or append to `.lensmcp/events.jsonl` directly, so refusing it would protect nothing and break
12
+ * every non-browser client. Whether it must also present a token is the server's decision.
13
+ * - `null` → an opaque origin: a sandboxed iframe, a `data:` or `file:` document — exactly how a page
14
+ * hides where it came from. Refused unless an entry says `null`.
15
+ * - a loopback name (`localhost`, `*.localhost`, `127.0.0.0/8`, `[::1]`; http or https, any port) →
16
+ * admitted: a page served by something already running on this machine, inside the trust boundary
17
+ * the bridge itself sits in.
18
+ * - a configured entry, or an origin the host LEARNED it serves (the Vite plugin learns the origin it
19
+ * handed its runtime to — the gateway's `https://app.local`, a LAN address) → admitted.
20
+ * - anything else → refused.
21
+ */
22
+ export interface OriginPolicy {
23
+ /** Whether an upgrade carrying this `Origin` may proceed; `undefined` = the upgrade carried none. */
24
+ admits(origin: string | undefined): boolean;
25
+ /** Admit an origin the host has seen itself serve. Bounded: the oldest learned origin goes first. */
26
+ learn(origin: string): void;
27
+ }
28
+ /**
29
+ * Most learned origins one policy keeps. A dev server is reached under a handful of names (localhost,
30
+ * a LAN address, a gateway host or two); the cap only matters to a client feeding it forged ones.
31
+ */
32
+ export declare const MAX_LEARNED_ORIGINS = 64;
33
+ /**
34
+ * Build a policy from allowlist entries. Each entry is one of:
35
+ * - a full origin — `https://app.test:8443` — matched exactly;
36
+ * - a hostname — `app.test` — any scheme and port on that host;
37
+ * - a subdomain wildcard — `.app.test` — that host and every subdomain of it (Vite's `allowedHosts` form);
38
+ * - `null` — admit opaque origins;
39
+ * - `*` — admit every http(s) origin (an explicit opt-out; `null` still needs its own entry).
40
+ * Entries that parse as none of these are ignored.
41
+ */
42
+ export declare function createOriginPolicy(entries?: readonly string[]): OriginPolicy;
43
+ /** Split a comma- or whitespace-separated allowlist (the `LENSMCP_BRIDGE_ALLOWED_ORIGINS` form). */
44
+ export declare function parseAllowedOrigins(value: string | undefined): string[];
45
+ /** `localhost`, any `*.localhost`, the whole `127.0.0.0/8` block, and `[::1]` (URL hostname form). */
46
+ export declare function isLoopbackHostname(hostname: string): boolean;
@@ -0,0 +1 @@
1
+ "use strict";var f=Object.defineProperty;var i=(t,e)=>f(t,"name",{value:e,configurable:!0});var d=Object.defineProperty,s=i((t,e)=>d(t,"name",{value:e,configurable:!0}),"s");import{capMap as h}from"@lensmcp/protocol-types";export const MAX_LEARNED_ORIGINS=64;export function createOriginPolicy(t=[]){let e=!1,n=!1;const u=[];for(const r of t){const o=r.trim().toLowerCase();if(o)if(o==="*")e=!0;else if(o==="null")n=!0;else{const a=p(o);a&&u.push(a)}}const c=new Map;return{admits(r){if(r===void 0)return!0;if(r==="null")return n;const o=l(r);return o?e||isLoopbackHostname(o.hostname)||c.has(o.origin)||u.some(a=>a(o)):!1},learn(r){const o=l(r);o&&(c.delete(o.origin),c.set(o.origin,!0),h(c,MAX_LEARNED_ORIGINS))}}}i(createOriginPolicy,"createOriginPolicy"),s(createOriginPolicy,"createOriginPolicy");export function parseAllowedOrigins(t){return(t??"").split(/[\s,]+/).filter(e=>e.length>0)}i(parseAllowedOrigins,"parseAllowedOrigins"),s(parseAllowedOrigins,"parseAllowedOrigins");export function isLoopbackHostname(t){const e=t.toLowerCase();return e==="localhost"||e.endsWith(".localhost")||e==="[::1]"||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(e)}i(isLoopbackHostname,"isLoopbackHostname"),s(isLoopbackHostname,"isLoopbackHostname");function l(t){try{const e=new URL(t);return e.protocol==="http:"||e.protocol==="https:"?e:void 0}catch{return}}i(l,"l"),s(l,"parseOrigin");function p(t){if(t.includes("://")){const e=l(t)?.origin;return e?n=>n.origin===e:void 0}if(t.startsWith(".")){const e=t.slice(1);return e?n=>n.hostname===e||n.hostname.endsWith(t):void 0}return e=>e.hostname===t}i(p,"d"),s(p,"ruleFor");
package/lib/server.d.ts CHANGED
@@ -21,12 +21,63 @@ export interface BridgeServerOptions extends BridgeDeps {
21
21
  host?: string;
22
22
  /** Bind port. `0` selects an ephemeral port; default {@link DEFAULT_WS_PORT}. */
23
23
  port?: number;
24
+ /**
25
+ * Browser origins admitted BEYOND loopback (entry forms: `origin-policy.ts`). Merged with
26
+ * `LENSMCP_BRIDGE_ALLOWED_ORIGINS`. A page from any other origin is refused at the upgrade
27
+ * (issues/closed/codex-security-32).
28
+ */
29
+ allowedOrigins?: readonly string[];
30
+ /**
31
+ * A shared secret every upgrade must carry as `?token=`. Unset, none is required — the standalone
32
+ * sidecar's zero-config default, because a no-build page has no way to learn a per-start secret. The
33
+ * Vite plugin always sets one and bakes it into the runtime it serves, so knowing the port is not
34
+ * enough to connect (issues/closed/codex-security-68).
35
+ */
36
+ token?: string;
37
+ /** Most sockets held open at once. Default {@link MAX_BRIDGE_CONNECTIONS}. */
38
+ maxConnections?: number;
39
+ /** Per-connection envelope budget. Default {@link CONNECTION_BURST} / {@link CONNECTION_PER_SEC}. */
40
+ connectionRateLimit?: ConnectionRateLimit;
41
+ /** Told about every refused upgrade. Default: one `console.warn` per distinct origin and status. */
42
+ onRefuse?: (refusal: BridgeRefusal) => void;
43
+ }
44
+ /** A token bucket over ENVELOPES (each one inside a batch counts), one per connection. */
45
+ export interface ConnectionRateLimit {
46
+ /** Envelopes a connection may send at once. */
47
+ burst: number;
48
+ /** Envelopes per second the bucket refills by. */
49
+ perSec: number;
50
+ }
51
+ /** An upgrade the bridge turned away. */
52
+ export interface BridgeRefusal {
53
+ /** 401 missing/wrong token, 403 origin not allowed, 503 connection cap reached. */
54
+ status: number;
55
+ reason: string;
56
+ /** The upgrade's `Origin`, when it carried one. */
57
+ origin?: string;
58
+ }
59
+ /** Per-connection hooks for {@link handleClientEnvelope}. */
60
+ export interface EnvelopeIngress {
61
+ /**
62
+ * Charge one envelope against the connection's budget; `false` drops it before it is mapped. Called
63
+ * once per envelope — every one inside a batch — and once for a frame that is no envelope at all, so
64
+ * junk spends the same budget as events.
65
+ */
66
+ admit?: () => boolean;
24
67
  }
25
68
  export interface BridgeServer {
26
69
  /** The actual bound port (resolved even when `port: 0` was requested). */
27
70
  readonly port: number;
28
71
  readonly host: string;
29
- /** Stop accepting connections and release the port. */
72
+ /** Admit a browser origin the host has seen itself serve (bounded — see `origin-policy.ts`). */
73
+ learnOrigin(origin: string): void;
74
+ /** Whether a browser upgrade carrying this `Origin` would pass the origin check. */
75
+ admitsOrigin(origin: string): boolean;
76
+ /**
77
+ * Stop accepting, drop every open page socket and pending HTTP connection, and release the port.
78
+ * Resolves once all of that has happened — an open tab does not hold it — and every socket's `gone`
79
+ * has been published. Idempotent: later calls share the first one's promise.
80
+ */
30
81
  close(): Promise<void>;
31
82
  }
32
83
  /**
@@ -40,7 +91,7 @@ export declare function makePublisher(deps: BridgeDeps): (event: BaseEvent) => v
40
91
  * forward the resulting event via `publish`. Exported so it can be
41
92
  * exercised over a real WebSocket without booting any bundler.
42
93
  */
43
- export declare function handleClientEnvelope(raw: string, publish: (event: BaseEvent) => void, sessionId?: string): void;
94
+ export declare function handleClientEnvelope(raw: string, publish: (event: BaseEvent) => void, sessionId?: string, ingress?: EnvelopeIngress): void;
44
95
  /** The tab a raw envelope belongs to, or undefined for a malformed/untagged one. */
45
96
  export declare function readTabId(raw: string): string | undefined;
46
97
  /**
@@ -53,5 +104,10 @@ export declare function readTabId(raw: string): string | undefined;
53
104
  * Resolves once the server is listening, with the actual bound `port`.
54
105
  */
55
106
  export declare function createBridgeServer(options?: BridgeServerOptions): Promise<BridgeServer>;
107
+ /**
108
+ * What a throttled connection is told, as a `console.warn` from its own tab. Longer than the 80 chars
109
+ * the console fingerprint reads, so every notice shares one fingerprint; the count is in `args[1]`.
110
+ */
111
+ export declare const CONNECTION_THROTTLE_NOTICE = "[lensmcp-bridge] per-connection rate limit: this tab sent envelopes faster than the bridge admits, and some were dropped";
56
112
  /** Allocate an ephemeral free port (used when `port: 0` is requested). */
57
113
  export declare function findFreePort(): Promise<number>;
package/lib/server.js CHANGED
@@ -1 +1 @@
1
- "use strict";var m=Object.defineProperty;var s=(r,e)=>m(r,"name",{value:e,configurable:!0});var v=Object.defineProperty,i=s((r,e)=>v(r,"name",{value:e,configurable:!0}),"o");import{createServer as y}from"node:net";import{WebSocketServer as h}from"ws";import{boundedClone as S,DEFAULT_RAW_BYTE_LIMIT as g,exceedsJsonByteBudget as E}from"@lensmcp/protocol-types";import{DEFAULT_WS_HOST as T,DEFAULT_WS_PORT as w,MAX_BATCH_EVENTS as A,MAX_FRAME_BYTES as _}from"./constants.js";import{resolveEnvSink as k}from"./env-sink.js";import{envelopeToEvent as P}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=k()),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.slice(0,A))!n||typeof n!="object"||!("kind"in n)||n.kind!=="batch"&&d(n,e,t);return}d(o,e,t)}}s(handleClientEnvelope,"handleClientEnvelope"),i(handleClientEnvelope,"handleClientEnvelope");function d(r,e,t){let o;try{o=P(r,t)}catch{return}if(o)try{e(p(o))}catch{}}s(d,"d"),i(d,"mapAndPublish");function p(r){const e=r.raw;return e==null||!E(e,g)?r:{...r,raw:S(e).value}}s(p,"v"),i(p,"boundRaw");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??T,t=r.port??w,o=t===0?await findFreePort():t,n=makePublisher(r),a=new h({host:e,port:o,maxPayload:_});return a.on("error",()=>{}),a.on("connection",c=>{let l;c.on("error",()=>{}),c.on("message",f=>{const u=f.toString();l??=readTabId(u),handleClientEnvelope(u,n,r.sessionId)}),c.on("close",()=>{l&&handleClientEnvelope(JSON.stringify({kind:"gone",at:Date.now(),tabId:l}),n,r.sessionId)})}),await new Promise((c,l)=>{const f=i(b=>{a.off("listening",u),l(b)},"onError"),u=i(()=>{a.off("error",f),c()},"onListening");a.once("error",f),a.once("listening",u)}),{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=y();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");
1
+ "use strict";var R=Object.defineProperty;var d=(e,r)=>R(e,"name",{value:r,configurable:!0});var L=Object.defineProperty,n=d((e,r)=>L(e,"name",{value:r,configurable:!0}),"t");import{createHash as B,timingSafeEqual as D}from"node:crypto";import{createServer as x,STATUS_CODES as $}from"node:http";import{createServer as U}from"node:net";import{WebSocket as F,WebSocketServer as M}from"ws";import{boundedClone as W,createEventFloodGuard as G,DEFAULT_RAW_BYTE_LIMIT as H,exceedsJsonByteBudget as J}from"@lensmcp/protocol-types";import{BRIDGE_TOKEN_PARAM as j,CONNECTION_BURST as q,CONNECTION_PER_SEC as z,DEFAULT_WS_HOST as V,DEFAULT_WS_PORT as X,ENV_BRIDGE_ALLOWED_ORIGINS as y,MAX_BATCH_EVENTS as K,MAX_BRIDGE_CONNECTIONS as Y,MAX_FRAME_BYTES as Q}from"./constants.js";import{resolveEnvSink as Z}from"./env-sink.js";import{asBaseEvent as ee,conformsToEnvelopeContract as re}from"./envelope-contract.js";import{envelopeToEvent as te}from"./event-mappers.js";import{createOriginPolicy as ne,parseAllowedOrigins as oe}from"./origin-policy.js";export function makePublisher(e){let r;return t=>{if(e.bus){e.bus.publish(t);return}if(e.sink){e.sink(t);return}if(r===void 0&&(r=Z()),r){r(t);return}(t.severity==="error"||t.severity==="fatal")&&console.debug(`[lensmcp-bridge] ${t.severity}: ${t.title}`)}}d(makePublisher,"makePublisher"),n(makePublisher,"makePublisher");export function handleClientEnvelope(e,r,t,a={}){const c=a.admit??se;let l;try{l=JSON.parse(e)}catch{c();return}if(!l||typeof l!="object"||!("kind"in l)){c();return}if(l.kind==="batch"){if(!Array.isArray(l.events)){c();return}for(const f of l.events.slice(0,K))c()&&(!f||typeof f!="object"||!("kind"in f)||f.kind!=="batch"&&E(f,r,t));return}c()&&E(l,r,t)}d(handleClientEnvelope,"handleClientEnvelope"),n(handleClientEnvelope,"handleClientEnvelope");const se=n(()=>!0,"admitAll");function E(e,r,t){if(!re(e))return;let a;try{const c=te(e,t);a=c&&ee(c)}catch{return}if(a)try{r(T(a))}catch{}}d(E,"O"),n(E,"mapAndPublish");function T(e){const r=e.raw;return r==null||!J(r,H)?e:{...e,raw:W(r).value}}d(T,"Z"),n(T,"boundRaw");export function readTabId(e){try{const r=JSON.parse(e);return typeof r?.tabId=="string"&&r.tabId?r.tabId:void 0}catch{return}}d(readTabId,"readTabId"),n(readTabId,"readTabId");export async function createBridgeServer(e={}){const r=e.host??V,t=e.port??X,a=makePublisher(e),c=ne([...e.allowedOrigins??[],...oe(process.env[y])]),l=e.token?S(e.token):void 0,f=e.maxConnections??Y,h=e.connectionRateLimit??{burst:q,perSec:z},k=e.onRefuse??N(),P=n(s=>{const i=s.headers.origin,o=i===void 0?{}:{origin:i};if(m.clients.size>=f)return{status:503,reason:"bridge connection limit reached",...o};if(!c.admits(i))return{status:403,reason:"origin not allowed",...o};if(l&&!I(s.url,l))return{status:401,reason:"missing or wrong bridge token",...o}},"refuseUpgrade"),u=x(w);u.on("error",()=>{});const m=new M({server:u,maxPayload:Q,verifyClient:n((s,i)=>{const o=P(s.req);if(!o){i(!0);return}try{k(o)}catch{}i(!1,o.status,o.reason)},"verifyClient")});m.on("error",()=>{}),m.on("connection",s=>{if(s.on("error",()=>{}),m.clients.size>f){s.close(1013,"bridge connection limit reached");return}let i;const o=G({burst:h.burst,perSec:h.perSec,envOverride:!1}),g=n(p=>{handleClientEnvelope(JSON.stringify({kind:"console",level:"warn",at:Date.now(),...i?{tabId:i}:{},args:[CONNECTION_THROTTLE_NOTICE,{dropped:p}]}),a,e.sessionId)},"announceDropped"),v={admit:n(()=>{const p=o.admit(ie);return p.ok&&p.suppressed>0&&g(p.suppressed),p.ok},"admit")};s.on("message",p=>{const _=p.toString();i??=readTabId(_),handleClientEnvelope(_,a,e.sessionId,v)}),s.on("close",()=>{i&&handleClientEnvelope(JSON.stringify({kind:"gone",at:Date.now(),tabId:i}),a,e.sessionId)})}),await new Promise((s,i)=>{const o=n(v=>{u.off("listening",g),i(v)},"onError"),g=n(()=>{u.off("error",o),s()},"onListening");u.once("error",o),u.once("listening",g),u.listen(t,r)});const b=u.address(),A=typeof b=="object"&&b!==null?b.port:t;let O;return{port:A,host:r,learnOrigin:n(s=>c.learn(s),"learnOrigin"),admitsOrigin:n(s=>c.admits(s),"admitsOrigin"),close:n(()=>(O??=(async()=>{const s=[...m.clients],i=[new Promise(o=>m.close(()=>o())),new Promise(o=>u.close(()=>o())),...s.map(C)];for(const o of s)o.terminate();u.closeAllConnections(),await Promise.all(i)})(),O),"close")}}d(createBridgeServer,"createBridgeServer"),n(createBridgeServer,"createBridgeServer");function w(e,r){const t=$[426]??"Upgrade Required";r.writeHead(426,{"Content-Length":Buffer.byteLength(t),"Content-Type":"text/plain"}),r.end(t)}d(w,"ee"),n(w,"answerPlainRequest");function C(e){return e.readyState===F.CLOSED?Promise.resolve():new Promise(r=>e.once("close",()=>r()))}d(C,"ne"),n(C,"socketClosed");const ie="connection";export const CONNECTION_THROTTLE_NOTICE="[lensmcp-bridge] per-connection rate limit: this tab sent envelopes faster than the bridge admits, and some were dropped";const ae=32;function N(){const e=new Set;return r=>{const t=`${r.status} ${r.origin??""}`;if(e.has(t)||e.size>=ae)return;e.add(t);const a=r.status===403?` \u2014 if this is your app, add its origin to ${y} (or the Vite plugin's \`wsAllowedOrigins\`)`:r.status===401?" \u2014 a tab opened before the dev server restarted needs a reload; a no-build page needs the token as __LENSMCP_WS_TOKEN__":"";console.warn(`[lensmcp-bridge] refused a connection${r.origin?` from ${r.origin}`:""}: ${r.reason} (${r.status})${a}`)}}d(N,"oe"),n(N,"warnRefusalOnce");function S(e){return B("sha256").update(e).digest()}d(S,"w"),n(S,"digest");function I(e,r){let t;try{t=new URL(e??"/","http://bridge.invalid").searchParams.get(j)}catch{return!1}return t!==null&&D(S(t),r)}d(I,"ie"),n(I,"carriesToken");export function findFreePort(){return new Promise((e,r)=>{const t=U();t.unref(),t.on("error",r),t.listen(0,()=>{const a=t.address();if(!a){t.close(),r(new Error("Could not allocate free port"));return}const c=a.port;t.close(()=>e(c))})})}d(findFreePort,"findFreePort"),n(findFreePort,"findFreePort");
package/lib/types.d.ts CHANGED
@@ -97,7 +97,7 @@ export type ClientEnvelope = (
97
97
  * (`triggeredBy`) and the path it reloaded for; `update` lists the modules that DID hot-update;
98
98
  * `invalidate` is a module giving up on self-acceptance; `ws-disconnect` / `ws-connect` are the
99
99
  * HMR socket itself — the "[vite] server connection lost" console line, as data. Without these the
100
- * lens was blind to ~320 full reloads a day on one workspace (issues/dev-gateway/08).
100
+ * lens was blind to ~320 full reloads a day on one workspace (issues/closed/dev-gateway-08).
101
101
  */
102
102
  | {
103
103
  kind: 'hmr';
package/main.js CHANGED
@@ -1 +1 @@
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();
1
+ "use strict";var S=Object.defineProperty;var i=(s,e)=>S(s,"name",{value:e,configurable:!0});var E=Object.defineProperty,l=i((s,e)=>E(s,"name",{value:e,configurable:!0}),"i");import{DEFAULT_WS_HOST as u,DEFAULT_WS_PORT as v,ENV_BRIDGE_ALLOWED_ORIGINS as t,ENV_BRIDGE_TOKEN as $,ENV_SESSION_ID as N,ENV_WS_HOST as b,ENV_WS_PORT as d,SHUTDOWN_GRACE_MS as m}from"./lib/constants.js";import{closeEnvSink as T}from"./lib/env-sink.js";import{createBridgeServer as f}from"./lib/server.js";async function g(){const s=process.env[b]??u,e=process.env[d],o=e!==void 0&&e!==""?Number(e):v;(!Number.isFinite(o)||o<0)&&(console.error(`[lensmcp-bridge] invalid ${d}=${e}`),process.exit(2));const _=process.env[N],c=process.env[$]||void 0;let r;try{r=await f({host:s,port:o,sessionId:_,token:c})}catch(n){console.error(`[lensmcp-bridge] failed to listen on ws://${s}:${o} \u2014 ${n.message}`),process.exit(1)}console.log(`[lensmcp-bridge] listening on ws://${r.host}:${r.port}`),console.log(`[lensmcp-bridge] admitting pages from loopback origins${process.env[t]?` and ${process.env[t]}`:` (set ${t} to add more)`}; token ${c?"required":"not required"}`);let a=!1;const p=l(n=>{a&&(console.error(`[lensmcp-bridge] ${n} again \u2014 exiting now`),process.exit(1)),a=!0,setTimeout(()=>{console.error(`[lensmcp-bridge] shutdown did not finish within ${m}ms \u2014 exiting anyway`),process.exit(1)},m),r.close().then(()=>T()).then(()=>process.exit(0),()=>process.exit(1))},"shutdown");process.on("SIGINT",p),process.on("SIGTERM",p)}i(g,"u"),l(g,"main"),g();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/bridge",
3
- "version": "1.23.13",
3
+ "version": "1.24.0",
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.23.13",
23
- "@lensmcp/protocol-types": "1.23.13",
22
+ "@lensmcp/core": "1.24.0",
23
+ "@lensmcp/protocol-types": "1.24.0",
24
24
  "tslib": "^2.3.0",
25
25
  "ws": "^8.21.0"
26
26
  },