@yoltra/devtools-server 0.2.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 ADDED
@@ -0,0 +1,160 @@
1
+ ![Yoltra logo](../../assets/yoltra-logo.png)
2
+
3
+ # @yoltra/devtools-server
4
+
5
+ > [ πŸ‡²πŸ‡½ VersiΓ³n en EspaΓ±ol](./README.es.md)  | πŸ‘‰ πŸ‡ΊπŸ‡Έ English Version  
6
+
7
+ **Central WebSocket hub that brokers DevTools protocol traffic between Yoltra stores and
8
+ extensions.**
9
+
10
+ `@yoltra/devtools-server` runs a localhost-only WebSocket server that handles protocol
11
+ handshakes, routes messages between stores and DevTools UIs, and maintains a ring buffer of
12
+ recent events for late-connecting extensions.
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @yoltra/devtools-server
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ### As a library
27
+
28
+ Embed the hub in your own process (test runner, dev server, VSCode extension):
29
+
30
+ ```typescript
31
+ import { DevtoolsHub } from "@yoltra/devtools-server";
32
+
33
+ const hub = new DevtoolsHub({ port: 9800 });
34
+ await hub.start();
35
+
36
+ console.log("Hub listening on ws://127.0.0.1:9800");
37
+ console.log("Connected stores:", hub.storeCount);
38
+ console.log("Connected extensions:", hub.extensionCount);
39
+
40
+ // Later...
41
+ await hub.stop();
42
+ ```
43
+
44
+ ### As a standalone CLI
45
+
46
+ ```bash
47
+ npx @yoltra/devtools-server --port 9800 --history-size 1000
48
+ ```
49
+
50
+ Or via the project binary:
51
+
52
+ ```bash
53
+ node ./bin/devtools-server.js --port 9800
54
+ ```
55
+
56
+ ---
57
+
58
+ ## How It Works
59
+
60
+ ```
61
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
62
+ β”‚ Yoltra β”‚ ──── β”‚ DevTools β”‚ ──── β”‚ DevTools UI β”‚
63
+ β”‚ Store β”‚ WS β”‚ Hub β”‚ WS β”‚ (Extension) β”‚
64
+ β”‚ β”‚ ───► β”‚ (this pkg) β”‚ ───► β”‚ β”‚
65
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
66
+ β”‚
67
+ Ring Buffer
68
+ (event history)
69
+ ```
70
+
71
+ 1. **Stores** connect and perform a protocol handshake
72
+ 2. Store events are **fanned out** to all connected extensions
73
+ 3. Extension commands (state requests, time travel) are **routed** to the target store by
74
+ `storeId`
75
+ 4. Recent events are **buffered** in a ring buffer so late-connecting extensions receive history
76
+
77
+ ---
78
+
79
+ ## Configuration
80
+
81
+ ```typescript
82
+ interface DevtoolsHubOptions {
83
+ /** Port to bind on. @default 9800 */
84
+ port?: number;
85
+ /** Host to bind on. @default "127.0.0.1" */
86
+ host?: string;
87
+ /** Maximum events retained for late-connecting extensions. @default 1000 */
88
+ historySize?: number;
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## API Reference
95
+
96
+ ### `DevtoolsHub`
97
+
98
+ | Method / Property | Description |
99
+ | ------------------------- | ------------------------------------------- |
100
+ | `new DevtoolsHub(opts?)` | Create a hub instance |
101
+ | `hub.start()` | Start the WS server (returns a Promise) |
102
+ | `hub.stop()` | Stop the server and close all connections |
103
+ | `DevtoolsHub.probe(port)` | Check if a hub is already running on a port |
104
+ | `hub.storeCount` | Number of connected stores |
105
+ | `hub.extensionCount` | Number of connected extensions |
106
+ | `hub.historySize` | Number of events in the ring buffer |
107
+
108
+ ### `RingBuffer<T>`
109
+
110
+ A fixed-size circular buffer used internally for event history:
111
+
112
+ ```typescript
113
+ import { RingBuffer } from "@yoltra/devtools-server";
114
+
115
+ const buf = new RingBuffer<string>(100);
116
+ buf.push("event-1");
117
+ buf.push("event-2");
118
+ buf.toArray(); // ['event-1', 'event-2']
119
+ buf.size; // 2
120
+ buf.clear();
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Probe Before Starting
126
+
127
+ Avoid port conflicts by checking if a hub is already running:
128
+
129
+ ```typescript
130
+ import { DevtoolsHub } from "@yoltra/devtools-server";
131
+
132
+ const alreadyRunning = await DevtoolsHub.probe(9800);
133
+
134
+ if (!alreadyRunning) {
135
+ const hub = new DevtoolsHub({ port: 9800 });
136
+ await hub.start();
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Security
143
+
144
+ The hub binds to `127.0.0.1` (localhost only) by default. This is a deliberate v1 security
145
+ constraint β€” the hub is not exposed to the network.
146
+
147
+ ---
148
+
149
+ ## Related Packages
150
+
151
+ - **[@yoltra/devtools-protocol](../devtools-protocol/README.md)** β€” Wire format and message
152
+ types
153
+ - **[@yoltra/devtools-browser-agent](../devtools-browser-agent/README.md)** β€” Connects browser
154
+ stores to this hub
155
+
156
+ ---
157
+
158
+ ## License
159
+
160
+ **MIT** β€” Free to use in commercial and open-source projects.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { startCli } from "../dist/devtools-server.esm.js";
3
+
4
+ startCli();
@@ -0,0 +1,9 @@
1
+ /*!
2
+ * @yoltra/devtools-server v0.2.0
3
+ * (c) 2026 Manu Ramirez <@pixerael>
4
+ * License: MIT
5
+ * Homepage: https://yoltra.dev
6
+ */
7
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("@yoltra/devtools-protocol"),u=require("ws");class h{constructor(t){if(this.capacity=t,this.head=0,this.count=0,t<1)throw new Error("RingBuffer capacity must be >= 1");this.items=new Array(t)}push(t){this.items[this.head]=t,this.head=(this.head+1)%this.capacity,this.count<this.capacity&&this.count++}toArray(){if(this.count===0)return[];const t=[],e=this.count<this.capacity?0:this.head;for(let s=0;s<this.count;s++)t.push(this.items[(e+s)%this.capacity]);return t}get size(){return this.count}clear(){this.items.fill(void 0),this.head=0,this.count=0}}class f{constructor(){this.stores=new Map,this.extensions=new Map}register(t){t.role===o.DevtoolsRole.STORE?this.stores.set(t.id,t):this.extensions.set(t.id,t)}unregister(t,e){e===o.DevtoolsRole.STORE?this.stores.delete(t):this.extensions.delete(t)}getStoreSocket(t){return this.stores.get(t)?.ws}fanOutToExtensions(t){for(const[,e]of this.extensions)e.ws.readyState===e.ws.OPEN&&e.ws.send(t)}sendToStore(t,e){const s=this.stores.get(t);return!s||s.ws.readyState!==s.ws.OPEN?!1:(s.ws.send(e),!0)}buildStoreConnectedMessage(t){if(!t.storeInfo)return null;const e={type:"STORE_CONNECTED",timestamp:new Date().toISOString(),sourceId:"hub",sourceRole:o.DevtoolsRole.HUB,store:{id:t.id,name:t.storeInfo.name,capabilities:t.storeInfo.capabilities}};return JSON.stringify(e)}buildStoreDisconnectedMessage(t,e){const s={type:"STORE_DISCONNECTED",timestamp:new Date().toISOString(),sourceId:"hub",sourceRole:o.DevtoolsRole.HUB,storeId:t,reason:e};return JSON.stringify(s)}buildRegistryMessage(){const t={type:"STORE_REGISTRY",timestamp:new Date().toISOString(),sourceId:"hub",sourceRole:o.DevtoolsRole.HUB,stores:Array.from(this.stores.values()).flatMap(e=>e.storeInfo?[{id:e.id,name:e.storeInfo.name,status:"connected",capabilities:e.storeInfo.capabilities,connectedAt:e.connectedAt}]:[])};return JSON.stringify(t)}get storeCount(){return this.stores.size}get extensionCount(){return this.extensions.size}}const p=5e3,S=8*1024*1024;function y(i,t){if(!i||t.includes(i))return!0;let e;try{e=new URL(i)}catch{return!1}return e.protocol==="chrome-extension:"||e.protocol==="moz-extension:"||e.protocol==="safari-web-extension:"?!0:O(e.hostname)}function O(i){const t=i.replace(/^\[|\]$/g,"");return t==="localhost"||t.endsWith(".localhost")||t==="127.0.0.1"||t.startsWith("127.")||t==="::1"||t==="0:0:0:0:0:0:0:1"}class d{constructor(t={}){this.router=new f,this.wss=null,this.port=t.port??9800,this.host=t.host??"127.0.0.1",this.allowedOrigins=t.allowedOrigins??[],this.history=new h(t.historySize??1e3)}async start(){return new Promise((t,e)=>{this.wss=new u.WebSocketServer({port:this.port,host:this.host,maxPayload:S,verifyClient:s=>y(s.origin,this.allowedOrigins)?!0:(console.warn(`[yoltra devtools] Rejected WebSocket connection from disallowed origin: ${s.origin}`),!1)}),this.wss.on("listening",()=>{t()}),this.wss.on("error",s=>{e(s)}),this.wss.on("connection",s=>{this.handleConnection(s)})})}async stop(){return new Promise(t=>{if(!this.wss){t();return}this.wss.close(()=>{this.wss=null,t()});for(const e of this.wss.clients)e.close(1001,"Hub shutting down")})}static async probe(t){return new Promise(e=>{const s=new u.WebSocket(`ws://127.0.0.1:${t}`),r=setTimeout(()=>{s.close(),e(!1)},2e3);s.on("open",()=>{clearTimeout(r),s.close(),e(!0)}),s.on("error",()=>{clearTimeout(r),e(!1)})})}handleConnection(t){let e=null;const s=setTimeout(()=>{e||t.close(1008,"Handshake timeout")},p);t.on("message",r=>{let n;try{n=JSON.parse(r.toString())}catch{return}if(!(n===null||typeof n!="object"||Array.isArray(n))&&typeof n.type=="string"){if(!e){n.type==="HANDSHAKE_REQUEST"&&(clearTimeout(s),e=this.handleHandshake(t,n),e||t.close(1008,"Handshake failed"));return}this.routeMessage(e,n)}}),t.on("close",()=>{clearTimeout(s),e&&this.handleDisconnect(e)}),t.on("error",()=>{})}handleHandshake(t,e){const s=parseInt(e.protocolVersion?.split(".")[0]??"0"),r=parseInt(o.PROTOCOL_VERSION.split(".")[0]);if(s!==r){const l={type:"HANDSHAKE_RESPONSE",success:!1,negotiatedVersion:o.PROTOCOL_VERSION,hubCapabilities:{maxHistorySize:this.history.capacity,supportedFeatures:[]},error:`Incompatible protocol version: ${e.protocolVersion} (hub: ${o.PROTOCOL_VERSION})`};return t.send(JSON.stringify(l)),null}const n=e.role===o.DevtoolsRole.STORE?e.store?.id:e.extension?.id;if(!n)return console.warn(`[yoltra devtools] Rejected handshake: role ${e.role} without a matching id payload`),null;const c={ws:t,role:e.role,id:n,connectedAt:new Date().toISOString()};e.role===o.DevtoolsRole.STORE&&e.store?c.storeInfo={name:e.store.name,capabilities:e.store.capabilities}:e.role===o.DevtoolsRole.EXTENSION&&e.extension&&(c.extensionInfo={name:e.extension.name,capabilities:e.extension.capabilities}),this.router.register(c);const a={type:"HANDSHAKE_RESPONSE",success:!0,negotiatedVersion:o.PROTOCOL_VERSION,hubCapabilities:{maxHistorySize:this.history.capacity,supportedFeatures:[]}};if(t.send(JSON.stringify(a)),e.role===o.DevtoolsRole.STORE){const l=this.router.buildStoreConnectedMessage(c);l&&this.router.fanOutToExtensions(l)}else if(e.role===o.DevtoolsRole.EXTENSION){t.send(this.router.buildRegistryMessage());for(const l of this.history.toArray())t.send(l)}return c}routeMessage(t,e){const s=JSON.stringify(e);if(t.role===o.DevtoolsRole.STORE)this.router.fanOutToExtensions(s),e.type==="STORE_EVENT"&&this.history.push(s);else{const r=e.storeId;r&&this.router.sendToStore(r,s)}}handleDisconnect(t){if(this.router.unregister(t.id,t.role),t.role===o.DevtoolsRole.STORE){const e=this.router.buildStoreDisconnectedMessage(t.id,"disconnected");this.router.fanOutToExtensions(e)}}get storeCount(){return this.router.storeCount}get extensionCount(){return this.router.extensionCount}get historySize(){return this.history.size}}async function g(i=process.argv){const t=i.indexOf("--port"),e=parseInt(i.find(a=>a.startsWith("--port="))?.split("=")[1]??(t!==-1?i[t+1]:void 0)??"9800"),s=i.indexOf("--history-size"),r=parseInt(i.find(a=>a.startsWith("--history-size="))?.split("=")[1]??(s!==-1?i[s+1]:void 0)??"1000"),n=new d({port:e,historySize:r}),c=async()=>{console.log(`
8
+ Shutting down DevTools hub...`),await n.stop(),process.exit(0)};process.on("SIGINT",c),process.on("SIGTERM",c);try{await n.start(),console.log(`Yoltra DevTools hub running on ws://127.0.0.1:${e}`),console.log(`History buffer: ${r} events`)}catch(a){console.error("Failed to start DevTools hub:",a),process.exit(1)}}exports.DevtoolsHub=d;exports.RingBuffer=h;exports.startCli=g;
9
+ //# sourceMappingURL=devtools-server.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-server.cjs.js","sources":["../src/ring-buffer.ts","../src/router.ts","../src/hub.ts","../src/cli.ts"],"sourcesContent":["/**\n * Fixed-size circular buffer for bounded event retention.\n *\n * @module @yoltra/devtools-server\n */\n\n/**\n * Fixed-size circular buffer that overwrites the oldest entry on overflow.\n *\n * @typeParam T - Item type stored in the buffer.\n *\n * @remarks\n * Used by the hub to retain event history for late-connecting extensions.\n * The buffer pre-allocates an array of the given capacity and uses modular\n * arithmetic to track insertion position, making {@link push} an O(1)\n * operation with no memory allocation after construction.\n *\n * @public\n */\nexport class RingBuffer<T> {\n private readonly items: Array<T | undefined>;\n private head = 0;\n private count = 0;\n\n /**\n * @param capacity - Maximum number of items. Must be at least 1.\n */\n constructor(public readonly capacity: number) {\n if (capacity < 1) throw new Error(\"RingBuffer capacity must be >= 1\");\n this.items = new Array(capacity);\n }\n\n /**\n * Push an item. Overwrites the oldest if at capacity.\n *\n * @param item - Item to add.\n *\n * @public\n */\n push(item: T): void {\n this.items[this.head] = item;\n this.head = (this.head + 1) % this.capacity;\n if (this.count < this.capacity) {\n this.count++;\n }\n }\n\n /**\n * Returns all items in insertion order (oldest first).\n *\n * @returns A new array containing buffered items from oldest to newest.\n *\n * @public\n */\n toArray(): T[] {\n if (this.count === 0) return [];\n const result: T[] = [];\n const start = this.count < this.capacity ? 0 : this.head;\n for (let i = 0; i < this.count; i++) {\n result.push(this.items[(start + i) % this.capacity] as T);\n }\n return result;\n }\n\n /**\n * Current number of items stored in the buffer.\n *\n * @returns A value between `0` and {@link capacity} inclusive.\n *\n * @public\n */\n get size(): number {\n return this.count;\n }\n\n /**\n * Remove all items.\n *\n * @public\n */\n clear(): void {\n this.items.fill(undefined);\n this.head = 0;\n this.count = 0;\n }\n}\n","/**\n * Message routing layer for the DevTools hub.\n *\n * @module @yoltra/devtools-server\n */\n\nimport {\n DevtoolsRole,\n type StoreConnected,\n type StoreDisconnected,\n type StoreRegistry,\n} from \"@yoltra/devtools-protocol\";\nimport type { WebSocket } from \"ws\";\nimport type { ConnectionInfo } from \"./connection\";\n\n/**\n * Routes DevTools protocol messages between stores and extensions.\n *\n * @remarks\n * The router maintains two parallel maps -- one for store connections and\n * one for extension connections -- and exposes helpers that implement the\n * three core routing patterns of the DevTools protocol:\n *\n * - **Fan-out**: Store messages are forwarded to every connected extension.\n * - **Targeted delivery**: Extension commands are routed to a specific\n * store identified by `storeId`.\n * - **Lifecycle broadcast**: `STORE_CONNECTED` / `STORE_DISCONNECTED`\n * events are broadcast to all extensions whenever a store joins or\n * leaves.\n *\n * @public\n */\nexport class Router {\n /** All store connections, keyed by store ID. */\n private readonly stores = new Map<string, ConnectionInfo>();\n /** All extension connections, keyed by extension ID. */\n private readonly extensions = new Map<string, ConnectionInfo>();\n\n /**\n * Register a newly handshaked connection.\n *\n * @param info - Connection info from the completed handshake.\n *\n * @public\n */\n register(info: ConnectionInfo): void {\n if (info.role === DevtoolsRole.STORE) {\n this.stores.set(info.id, info);\n } else {\n this.extensions.set(info.id, info);\n }\n }\n\n /**\n * Remove a connection by ID.\n *\n * @param id - Client ID to remove.\n * @param role - Client role (`STORE` or `EXTENSION`).\n *\n * @public\n */\n unregister(id: string, role: DevtoolsRole): void {\n if (role === DevtoolsRole.STORE) {\n this.stores.delete(id);\n } else {\n this.extensions.delete(id);\n }\n }\n\n /**\n * Get the WebSocket for a specific store.\n *\n * @param storeId - Store UUID.\n * @returns The store's WebSocket, or `undefined` if not connected.\n *\n * @public\n */\n getStoreSocket(storeId: string): WebSocket | undefined {\n return this.stores.get(storeId)?.ws;\n }\n\n /**\n * Route a message from a store to all extensions (fan-out).\n *\n * @remarks\n * Only sends to extensions whose WebSocket is in the `OPEN` ready-state;\n * connections in a closing or closed state are silently skipped.\n *\n * @param message - Serialized JSON message string.\n *\n * @public\n */\n fanOutToExtensions(message: string): void {\n for (const [, ext] of this.extensions) {\n if (ext.ws.readyState === ext.ws.OPEN) {\n ext.ws.send(message);\n }\n }\n }\n\n /**\n * Route a message from an extension to a specific store.\n *\n * @param storeId - Target store UUID.\n * @param message - Serialized JSON message string.\n * @returns `true` if the message was sent, `false` if the store was\n * not found or its socket was not open.\n *\n * @public\n */\n sendToStore(storeId: string, message: string): boolean {\n const store = this.stores.get(storeId);\n if (!store || store.ws.readyState !== store.ws.OPEN) return false;\n store.ws.send(message);\n return true;\n }\n\n /**\n * Build a `STORE_CONNECTED` broadcast message.\n *\n * @param info - Store connection info (must have {@link ConnectionInfo.storeInfo}).\n * @returns Serialized {@link StoreConnected} JSON string.\n *\n * @public\n */\n buildStoreConnectedMessage(info: ConnectionInfo): string | null {\n // Only a fully-registered STORE connection carries storeInfo. Guard instead\n // of asserting so an incomplete registration can't crash the hub; the caller\n // skips fan-out when this returns null.\n if (!info.storeInfo) return null;\n const msg: StoreConnected = {\n type: \"STORE_CONNECTED\",\n timestamp: new Date().toISOString(),\n sourceId: \"hub\",\n sourceRole: DevtoolsRole.HUB,\n store: {\n id: info.id,\n name: info.storeInfo.name,\n capabilities: info.storeInfo.capabilities,\n },\n };\n return JSON.stringify(msg);\n }\n\n /**\n * Build a `STORE_DISCONNECTED` broadcast message.\n *\n * @param storeId - Disconnected store ID.\n * @param reason - Optional human-readable disconnect reason.\n * @returns Serialized {@link StoreDisconnected} JSON string.\n *\n * @public\n */\n buildStoreDisconnectedMessage(storeId: string, reason?: string): string {\n const msg: StoreDisconnected = {\n type: \"STORE_DISCONNECTED\",\n timestamp: new Date().toISOString(),\n sourceId: \"hub\",\n sourceRole: DevtoolsRole.HUB,\n storeId,\n reason,\n };\n return JSON.stringify(msg);\n }\n\n /**\n * Build a `STORE_REGISTRY` message listing all connected stores.\n *\n * @returns Serialized {@link StoreRegistry} JSON string.\n *\n * @public\n */\n buildRegistryMessage(): string {\n const msg: StoreRegistry = {\n type: \"STORE_REGISTRY\",\n timestamp: new Date().toISOString(),\n sourceId: \"hub\",\n sourceRole: DevtoolsRole.HUB,\n stores: Array.from(this.stores.values()).flatMap((s) => {\n // Skip connections whose registration hasn't completed (no storeInfo).\n if (!s.storeInfo) return [];\n return [\n {\n id: s.id,\n name: s.storeInfo.name,\n status: \"connected\" as const,\n capabilities: s.storeInfo.capabilities,\n connectedAt: s.connectedAt,\n },\n ];\n }),\n };\n return JSON.stringify(msg);\n }\n\n /**\n * Number of connected stores.\n *\n * @returns Current store connection count.\n *\n * @public\n */\n get storeCount(): number {\n return this.stores.size;\n }\n\n /**\n * Number of connected extensions.\n *\n * @returns Current extension connection count.\n *\n * @public\n */\n get extensionCount(): number {\n return this.extensions.size;\n }\n}\n","/**\n * Central WebSocket hub that brokers DevTools protocol traffic.\n *\n * @module @yoltra/devtools-server\n */\n\nimport {\n DevtoolsRole,\n PROTOCOL_VERSION,\n type HandshakeRequest,\n type HandshakeResponse,\n} from \"@yoltra/devtools-protocol\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type { ConnectionInfo } from \"./connection\";\nimport { RingBuffer } from \"./ring-buffer\";\nimport { Router } from \"./router\";\n\n/**\n * Configuration for the DevTools hub server.\n *\n * @remarks\n * All fields are optional; sensible defaults are applied when omitted.\n *\n * @public\n */\nexport interface DevtoolsHubOptions {\n /** Port to bind on. @default 9800 */\n port?: number;\n /** Host to bind on. @default \"127.0.0.1\" (localhost only for v1 security) */\n host?: string;\n /** Maximum events retained in the ring buffer for late-connecting extensions. @default 1000 */\n historySize?: number;\n /**\n * Extra WebSocket `Origin` values to accept, beyond the always-allowed set\n * (no Origin, browser-extension origins, and loopback origins). Use this only\n * for a non-loopback local dev host (e.g. a custom `.local` domain). Adding a\n * remote origin re-opens the cross-site hijack surface β€” don't.\n */\n allowedOrigins?: string[];\n}\n\n/**\n * Timeout for receiving a handshake request after a WebSocket connection\n * is established, in milliseconds.\n *\n * @remarks\n * If the client does not send a valid `HANDSHAKE_REQUEST` within this\n * window the connection is closed with code `1008` (Policy Violation).\n *\n * @internal\n */\nconst HANDSHAKE_TIMEOUT_MS = 5_000;\n\n/**\n * Maximum accepted WebSocket frame size (bytes). Frames fan out to every\n * extension and buffer into history, so an unbounded size is a local\n * DoS / memory-amplification vector. 8 MiB comfortably covers real state\n * snapshots while rejecting hostile oversized frames.\n */\nconst MAX_WS_PAYLOAD_BYTES = 8 * 1024 * 1024;\n\n/**\n * Whether a WebSocket `Origin` may connect to the hub.\n *\n * @remarks\n * The hub binds to loopback, but that does not stop a page you visit from\n * opening `ws://127.0.0.1:<port>` β€” WebSockets are exempt from same-origin/CORS,\n * so a remote page could otherwise exfiltrate state and drive the store. We\n * allow only: no Origin (node agent, CLI, some extension contexts), browser\n * extension origins (the user-installed panel), loopback origins (the local dev\n * app running the agent, or a local storeview), and any explicitly configured\n * origins. A remote origin (e.g. `https://evil.com`) is rejected.\n *\n * @internal\n */\nfunction isOriginAllowed(origin: string | undefined, allowed: readonly string[]): boolean {\n if (!origin) return true; // non-browser client; not reachable from a web page\n if (allowed.includes(origin)) return true;\n let url: URL;\n try {\n url = new URL(origin);\n } catch {\n return false;\n }\n if (\n url.protocol === \"chrome-extension:\" ||\n url.protocol === \"moz-extension:\" ||\n url.protocol === \"safari-web-extension:\"\n ) {\n return true;\n }\n return isLoopbackHost(url.hostname);\n}\n\n/** Loopback host check: `localhost`, the 127.0.0.0/8 block, and IPv6 `::1`. @internal */\nfunction isLoopbackHost(hostname: string): boolean {\n const h = hostname.replace(/^\\[|\\]$/g, \"\"); // strip IPv6 brackets\n return (\n h === \"localhost\" ||\n h.endsWith(\".localhost\") ||\n h === \"127.0.0.1\" ||\n h.startsWith(\"127.\") ||\n h === \"::1\" ||\n h === \"0:0:0:0:0:0:0:1\"\n );\n}\n\n/**\n * Central WebSocket hub that brokers messages between Yoltra stores and DevTools extensions.\n *\n * @remarks\n * - Accepts WS connections, validates protocol handshakes, and routes messages.\n * - Store events are fan-out to all extension clients.\n * - Extension commands are routed to the target store by `storeId`.\n * - Maintains a ring buffer of recent events for late-connecting extensions.\n * - Binds to localhost only (v1 security).\n *\n * @example Embeddable usage\n * ```ts\n * import { DevtoolsHub } from '@yoltra/devtools-server';\n *\n * const hub = new DevtoolsHub({ port: 9800 });\n * await hub.start();\n * // ... later\n * await hub.stop();\n * ```\n *\n * @public\n */\nexport class DevtoolsHub {\n private readonly port: number;\n private readonly host: string;\n private readonly allowedOrigins: readonly string[];\n private readonly router = new Router();\n private readonly history: RingBuffer<string>;\n private wss: WebSocketServer | null = null;\n\n /**\n * Create a new DevTools hub instance.\n *\n * @param opts - Hub configuration. All fields are optional.\n *\n * @public\n */\n constructor(opts: DevtoolsHubOptions = {}) {\n this.port = opts.port ?? 9800;\n this.host = opts.host ?? \"127.0.0.1\";\n this.allowedOrigins = opts.allowedOrigins ?? [];\n this.history = new RingBuffer<string>(opts.historySize ?? 1000);\n }\n\n /**\n * Start the WebSocket server and begin accepting connections.\n *\n * @returns Resolves once the server is bound and listening.\n * @throws If the underlying `WebSocketServer` emits an error during\n * startup (e.g. port already in use).\n *\n * @public\n */\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.wss = new WebSocketServer({\n port: this.port,\n host: this.host,\n // Bound the frame size (DEV-1): oversized frames fan out to every\n // extension and buffer into history, so an unbounded cap is a local\n // DoS / memory-amplification vector.\n maxPayload: MAX_WS_PAYLOAD_BYTES,\n // Reject cross-site WebSocket hijacking: the loopback bind alone does\n // not stop a page you visit from opening ws://127.0.0.1:<port>.\n verifyClient: (info: { origin?: string }) => {\n if (isOriginAllowed(info.origin, this.allowedOrigins)) return true;\n console.warn(\n `[yoltra devtools] Rejected WebSocket connection from disallowed origin: ${info.origin}`,\n );\n return false;\n },\n });\n\n this.wss.on(\"listening\", () => {\n resolve();\n });\n\n this.wss.on(\"error\", (err) => {\n reject(err);\n });\n\n this.wss.on(\"connection\", (ws) => {\n this.handleConnection(ws);\n });\n });\n }\n\n /**\n * Stop the server and close all connections.\n *\n * @remarks\n * Existing client sockets are closed with code `1001` (\"Going Away\")\n * before the server socket is torn down.\n *\n * @returns Resolves once the server has fully shut down.\n *\n * @public\n */\n async stop(): Promise<void> {\n return new Promise((resolve) => {\n if (!this.wss) {\n resolve();\n return;\n }\n this.wss.close(() => {\n this.wss = null;\n resolve();\n });\n // Close all existing connections\n for (const client of this.wss.clients) {\n client.close(1001, \"Hub shutting down\");\n }\n });\n }\n\n /**\n * Check if a DevTools hub is already running on the given port.\n *\n * @param port - Port to probe.\n * @returns `true` if a hub is listening and responds to handshake.\n *\n * @public\n */\n static async probe(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const ws = new WebSocket(`ws://127.0.0.1:${port}`);\n const timeout = setTimeout(() => {\n ws.close();\n resolve(false);\n }, 2_000);\n\n ws.on(\"open\", () => {\n clearTimeout(timeout);\n ws.close();\n resolve(true);\n });\n\n ws.on(\"error\", () => {\n clearTimeout(timeout);\n resolve(false);\n });\n });\n }\n\n /**\n * Handle a new WebSocket connection: wait for handshake, then route messages.\n *\n * @remarks\n * Starts a handshake timeout timer. If the first valid message is a\n * `HANDSHAKE_REQUEST` the connection is promoted to a routed client;\n * otherwise it is closed after {@link HANDSHAKE_TIMEOUT_MS}.\n *\n * @param ws - Newly accepted WebSocket.\n */\n private handleConnection(ws: WebSocket): void {\n let connectionInfo: ConnectionInfo | null = null;\n\n // Handshake timeout: close if no handshake within 5s\n const handshakeTimer = setTimeout(() => {\n if (!connectionInfo) {\n ws.close(1008, \"Handshake timeout\");\n }\n }, HANDSHAKE_TIMEOUT_MS);\n\n ws.on(\"message\", (data) => {\n let parsed: any;\n try {\n parsed = JSON.parse(data.toString());\n } catch {\n return; // Ignore malformed messages\n }\n\n // Ingress validation (DEV-3): every protocol message is a plain object\n // with a string `type` discriminant. Reject anything else (null, arrays,\n // primitives, missing type) before it reaches handshake/routing.\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return;\n if (typeof parsed.type !== \"string\") return;\n\n // Handle handshake\n if (!connectionInfo) {\n if (parsed.type === \"HANDSHAKE_REQUEST\") {\n clearTimeout(handshakeTimer);\n connectionInfo = this.handleHandshake(ws, parsed as HandshakeRequest);\n if (!connectionInfo) {\n ws.close(1008, \"Handshake failed\");\n }\n }\n return;\n }\n\n // Route messages based on role\n this.routeMessage(connectionInfo, parsed);\n });\n\n ws.on(\"close\", () => {\n clearTimeout(handshakeTimer);\n if (connectionInfo) {\n this.handleDisconnect(connectionInfo);\n }\n });\n\n ws.on(\"error\", () => {\n // Error is followed by close event, handled there\n });\n }\n\n /**\n * Process a handshake request: validate, register, and respond.\n *\n * @remarks\n * Performs a major-version compatibility check against\n * {@link PROTOCOL_VERSION}. On success the connection is registered with\n * the {@link Router} and post-handshake side-effects are triggered\n * (store-connected broadcast or registry + history replay).\n *\n * @param ws - The client WebSocket.\n * @param req - Parsed handshake request payload.\n * @returns The new {@link ConnectionInfo} on success, or `null` if the\n * handshake was rejected.\n */\n private handleHandshake(ws: WebSocket, req: HandshakeRequest): ConnectionInfo | null {\n // Basic protocol version check (accept same major version)\n const reqMajor = parseInt(req.protocolVersion?.split(\".\")[0] ?? \"0\");\n const ourMajor = parseInt(PROTOCOL_VERSION.split(\".\")[0]);\n if (reqMajor !== ourMajor) {\n const response: HandshakeResponse = {\n type: \"HANDSHAKE_RESPONSE\",\n success: false,\n negotiatedVersion: PROTOCOL_VERSION,\n hubCapabilities: {\n maxHistorySize: this.history.capacity,\n supportedFeatures: [],\n },\n error: `Incompatible protocol version: ${req.protocolVersion} (hub: ${PROTOCOL_VERSION})`,\n };\n ws.send(JSON.stringify(response));\n return null;\n }\n\n // A STORE handshake must carry `store`, an EXTENSION handshake `extension`.\n // Guard the role/payload match instead of dereferencing a missing field.\n const id = req.role === DevtoolsRole.STORE ? req.store?.id : req.extension?.id;\n if (!id) {\n console.warn(\n `[yoltra devtools] Rejected handshake: role ${req.role} without a matching id payload`,\n );\n return null;\n }\n\n // Build connection info\n const info: ConnectionInfo = {\n ws,\n role: req.role,\n id,\n connectedAt: new Date().toISOString(),\n };\n\n if (req.role === DevtoolsRole.STORE && req.store) {\n info.storeInfo = {\n name: req.store.name,\n capabilities: req.store.capabilities,\n };\n } else if (req.role === DevtoolsRole.EXTENSION && req.extension) {\n info.extensionInfo = {\n name: req.extension.name,\n capabilities: req.extension.capabilities,\n };\n }\n\n // Register in router\n this.router.register(info);\n\n // Send handshake response\n const response: HandshakeResponse = {\n type: \"HANDSHAKE_RESPONSE\",\n success: true,\n negotiatedVersion: PROTOCOL_VERSION,\n hubCapabilities: {\n maxHistorySize: this.history.capacity,\n supportedFeatures: [],\n },\n };\n ws.send(JSON.stringify(response));\n\n // Post-handshake actions\n if (req.role === DevtoolsRole.STORE) {\n // Broadcast STORE_CONNECTED to all extensions\n const connectMsg = this.router.buildStoreConnectedMessage(info);\n if (connectMsg) this.router.fanOutToExtensions(connectMsg);\n } else if (req.role === DevtoolsRole.EXTENSION) {\n // Send current store registry to the new extension\n ws.send(this.router.buildRegistryMessage());\n // Send buffered event history\n for (const msg of this.history.toArray()) {\n ws.send(msg);\n }\n }\n\n return info;\n }\n\n /**\n * Route a post-handshake message based on the sender's role.\n *\n * @remarks\n * Store messages are fanned-out to all extensions and, if the message\n * type is `STORE_EVENT`, buffered in the ring buffer for replay.\n * Extension messages are forwarded to the store identified by\n * `msg.storeId`.\n *\n * @param sender - Connection info of the sending client.\n * @param msg - Parsed message payload (untyped; serialized internally).\n */\n private routeMessage(sender: ConnectionInfo, msg: any): void {\n const raw = JSON.stringify(msg);\n\n if (sender.role === DevtoolsRole.STORE) {\n // Store messages β†’ fan-out to all extensions\n this.router.fanOutToExtensions(raw);\n\n // Buffer STORE_EVENT messages in the ring buffer\n if (msg.type === \"STORE_EVENT\") {\n this.history.push(raw);\n }\n } else {\n // Extension commands β†’ route to target store\n const storeId = msg.storeId as string | undefined;\n if (storeId) {\n this.router.sendToStore(storeId, raw);\n }\n }\n }\n\n /**\n * Handle a client disconnection.\n *\n * @remarks\n * Unregisters the client from the {@link Router}. If the client was a\n * store, a `STORE_DISCONNECTED` event is broadcast to all extensions.\n *\n * @param info - Connection info of the disconnected client.\n */\n private handleDisconnect(info: ConnectionInfo): void {\n this.router.unregister(info.id, info.role);\n\n if (info.role === DevtoolsRole.STORE) {\n // Broadcast STORE_DISCONNECTED to all extensions\n const disconnectMsg = this.router.buildStoreDisconnectedMessage(info.id, \"disconnected\");\n this.router.fanOutToExtensions(disconnectMsg);\n }\n }\n\n /**\n * Current number of connected stores.\n *\n * @public\n */\n get storeCount(): number {\n return this.router.storeCount;\n }\n\n /**\n * Current number of connected extensions.\n *\n * @public\n */\n get extensionCount(): number {\n return this.router.extensionCount;\n }\n\n /**\n * Number of events in the history ring buffer.\n *\n * @public\n */\n get historySize(): number {\n return this.history.size;\n }\n}\n","/**\n * CLI entry-point for the standalone DevTools hub process.\n *\n * @module @yoltra/devtools-server\n */\n\nimport { DevtoolsHub } from \"./hub\";\n\n/**\n * Parse CLI arguments and start the hub server.\n *\n * @remarks\n * Supported flags:\n *\n * | Flag | Default | Description |\n * | ------------------ | ------- | ---------------------------------- |\n * | `--port` | `9800` | WebSocket port to bind on. |\n * | `--history-size` | `1000` | Ring-buffer capacity for replays. |\n *\n * The function installs `SIGINT` and `SIGTERM` handlers for graceful\n * shutdown and exits with code `1` if the server fails to start.\n *\n * Usage: `npx @yoltra/devtools-server [--port 9800] [--history-size 1000]`\n *\n * @param argv - Argument vector to parse. Defaults to `process.argv`.\n * @returns Resolves once the hub is listening; never resolves during\n * normal operation (the process stays alive until a signal).\n *\n * @public\n */\nexport async function main(argv: string[] = process.argv): Promise<void> {\n const portIdx = argv.indexOf(\"--port\");\n const port = parseInt(\n argv.find((a) => a.startsWith(\"--port=\"))?.split(\"=\")[1] ??\n (portIdx !== -1 ? argv[portIdx + 1] : undefined) ??\n \"9800\",\n );\n\n const histIdx = argv.indexOf(\"--history-size\");\n const historySize = parseInt(\n argv.find((a) => a.startsWith(\"--history-size=\"))?.split(\"=\")[1] ??\n (histIdx !== -1 ? argv[histIdx + 1] : undefined) ??\n \"1000\",\n );\n\n const hub = new DevtoolsHub({ port, historySize });\n\n // Graceful shutdown\n const shutdown = async () => {\n console.log(\"\\nShutting down DevTools hub...\");\n await hub.stop();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n try {\n await hub.start();\n console.log(`Yoltra DevTools hub running on ws://127.0.0.1:${port}`);\n console.log(`History buffer: ${historySize} events`);\n } catch (err) {\n console.error(\"Failed to start DevTools hub:\", err);\n process.exit(1);\n }\n}\n"],"names":["RingBuffer","capacity","item","result","start","i","Router","info","DevtoolsRole","id","role","storeId","message","ext","store","msg","reason","s","HANDSHAKE_TIMEOUT_MS","MAX_WS_PAYLOAD_BYTES","isOriginAllowed","origin","allowed","url","isLoopbackHost","hostname","h","DevtoolsHub","opts","resolve","reject","WebSocketServer","err","ws","client","port","WebSocket","timeout","connectionInfo","handshakeTimer","data","parsed","req","reqMajor","ourMajor","PROTOCOL_VERSION","response","connectMsg","sender","raw","disconnectMsg","main","argv","portIdx","histIdx","historySize","hub","shutdown"],"mappings":"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"}