@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 +38 -2
- package/index.d.ts +5 -3
- package/index.js +1 -1
- package/lib/constants.d.ts +48 -2
- package/lib/constants.js +1 -1
- package/lib/env-sink.d.ts +8 -0
- package/lib/env-sink.js +3 -3
- package/lib/envelope-contract.d.ts +5 -0
- package/lib/envelope-contract.js +1 -0
- package/lib/origin-policy.d.ts +46 -0
- package/lib/origin-policy.js +1 -0
- package/lib/server.d.ts +58 -2
- package/lib/server.js +1 -1
- package/lib/types.d.ts +1 -1
- package/main.js +1 -1
- package/package.json +3 -3
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
|
-
|
|
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";
|
package/lib/constants.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
2
|
-
`;
|
|
3
|
-
`);
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
"@lensmcp/protocol-types": "1.
|
|
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
|
},
|