@buoy-gg/sync-broker 4.0.1

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/LICENSE ADDED
@@ -0,0 +1,58 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2024-present Buoy. All rights reserved.
4
+
5
+ This software and its source code are proprietary and confidential.
6
+
7
+ NOTICE: This is NOT open source software. This software is licensed,
8
+ not sold, and is protected by copyright laws and international treaties.
9
+
10
+ TERMS AND CONDITIONS:
11
+
12
+ 1. LICENSE GRANT
13
+ Subject to the terms of this Agreement and payment of applicable fees,
14
+ Buoy grants you a limited, non-exclusive, non-transferable license
15
+ to use the compiled software packages in your applications.
16
+
17
+ 2. RESTRICTIONS
18
+ You may NOT:
19
+ - Copy, modify, or distribute the source code
20
+ - Reverse engineer, decompile, or disassemble the software
21
+ - Remove or alter any proprietary notices or labels
22
+ - Sublicense, rent, lease, or lend the software
23
+ - Use the software to create competing products
24
+ - Share access credentials with unauthorized parties
25
+
26
+ 3. OWNERSHIP
27
+ React Buoy retains all right, title, and interest in the software,
28
+ including all intellectual property rights. This license does not
29
+ grant you any rights to trademarks or service marks.
30
+
31
+ 4. TERMINATION
32
+ This license is effective until terminated. Your rights under this
33
+ license will terminate automatically without notice if you fail to
34
+ comply with any of its terms. Upon termination, you must cease all
35
+ use and destroy all copies of the software.
36
+
37
+ 5. DISCLAIMER OF WARRANTIES
38
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
39
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
40
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
41
+
42
+ 6. LIMITATION OF LIABILITY
43
+ IN NO EVENT SHALL BUOY BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
44
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN
45
+ CONNECTION WITH THIS LICENSE OR THE USE OF THE SOFTWARE.
46
+
47
+ 7. GOVERNING LAW
48
+ This Agreement shall be governed by and construed in accordance with
49
+ the laws of the United States, without regard to its conflict of
50
+ law provisions.
51
+
52
+ For licensing inquiries and subscription information:
53
+ - Website: https://buoy.gg
54
+ - Email: AustinLovesWorking@gmail.com
55
+
56
+ Unauthorized reproduction or distribution of this software, or any
57
+ portion of it, may result in severe civil and criminal penalties,
58
+ and will be prosecuted to the maximum extent possible under the law.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @buoy-gg/sync-broker
2
+
3
+ Headless Socket.IO **broker** for the Buoy devtool sync protocol (v1).
4
+
5
+ This is the message router that sits between **devices** (React Native apps running a Buoy devtool, connected as Socket.IO clients) and **dashboard clients** (the Buoy desktop app, and the Buoy MCP server). It was extracted from the desktop app's `apps/desktop/src/server` so the same implementation can be hosted by either:
6
+
7
+ - the **desktop app** (which owns its own express/http server), or
8
+ - a **standalone process** such as the Buoy MCP server, when no desktop app is running.
9
+
10
+ ## Usage
11
+
12
+ ### Host a complete broker (standalone / MCP)
13
+
14
+ ```ts
15
+ import { createBroker, DEFAULT_BROKER_PORT } from "@buoy-gg/sync-broker";
16
+
17
+ const broker = createBroker({
18
+ port: DEFAULT_BROKER_PORT, // 42831
19
+ onListening: (port) => console.log(`broker listening on ${port}`),
20
+ onError: (err) => {
21
+ if (err.code === "EADDRINUSE") {
22
+ // Another broker (e.g. the desktop app) already owns the port —
23
+ // connect to it as a client instead of hosting your own.
24
+ }
25
+ },
26
+ });
27
+
28
+ // later
29
+ await broker.close();
30
+ ```
31
+
32
+ ### Attach to an existing Socket.IO server (desktop app)
33
+
34
+ ```ts
35
+ import { Server } from "socket.io";
36
+ import { socketHandle } from "@buoy-gg/sync-broker";
37
+
38
+ const io = new Server(httpServer, socketOptions);
39
+ socketHandle({ io });
40
+ ```
41
+
42
+ ## Protocol
43
+
44
+ The wire contract (events, message shapes, watch/backpressure model) is **v1**, canonically defined in [`@buoy-gg/external-sync`](../external-sync). The broker reproduces the server-relevant types in `src/protocol.ts` to stay a dependency-light Node package (no React Native / `socket.io-client`).
45
+
46
+ Topology and message flow:
47
+
48
+ - Devices connect with a handshake carrying `deviceId`, `platform`, etc.
49
+ - Dashboard clients connect with `deviceName: "Dashboard"` (exact, no suffix).
50
+ - Devices announce `devtool-capabilities`; the broker replays them to dashboards.
51
+ - Dashboards `devtool-watch` tools; the broker **consolidates** watch counts so a device receives a single `watching` true/false per `(device, tool)` regardless of how many dashboards are watching.
52
+ - Dashboards invoke `devtool-action`; devices reply `devtool-action-result`.
53
+ - Devices push `devtool-sync` snapshots; the broker forwards them to dashboards.
54
+
55
+ > **Security:** dev-only. No authentication, CORS is open (`*`), intended for loopback / trusted LAN.
@@ -0,0 +1,167 @@
1
+ import { Server, ServerOptions } from 'socket.io';
2
+ import http from 'http';
3
+
4
+ type DefaultEventsMap = Record<string, (...args: any[]) => void>;
5
+ interface Props {
6
+ io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
7
+ }
8
+ /**
9
+ * Server-side socket handler that manages:
10
+ * - Communication between devices and dashboard
11
+ * - User connection tracking
12
+ * - Query state synchronization
13
+ * - Action forwarding
14
+ */
15
+ declare function socketHandle({ io }: Props): void;
16
+
17
+ /** Default port the Buoy sync broker listens on. */
18
+ declare const DEFAULT_BROKER_PORT = 42831;
19
+ /**
20
+ * Socket.IO server config shared by every broker host (the desktop app and the
21
+ * standalone/MCP host). Mirrors the desktop's historical config.
22
+ */
23
+ declare const SOCKET_CONFIG: Partial<ServerOptions>;
24
+ interface CreateBrokerOptions {
25
+ /** Port to listen on. @default DEFAULT_BROKER_PORT (42831) */
26
+ port?: number;
27
+ /**
28
+ * Extra Socket.IO server options merged over {@link SOCKET_CONFIG}. Use to
29
+ * override CORS, transports, etc. for a specific host.
30
+ */
31
+ socketOptions?: Partial<ServerOptions>;
32
+ /** Called once the broker is listening. */
33
+ onListening?: (port: number) => void;
34
+ /**
35
+ * Called on a server error. `EADDRINUSE` means another broker (e.g. the
36
+ * desktop app) already owns the port — the caller should connect to it as a
37
+ * client instead of hosting its own. Headless on purpose: no UI/dialog here,
38
+ * unlike the Electron app which surfaces a native dialog.
39
+ */
40
+ onError?: (error: NodeJS.ErrnoException) => void;
41
+ }
42
+ interface BrokerHandle {
43
+ /** The underlying Socket.IO server. */
44
+ io: Server;
45
+ /** The underlying HTTP server. */
46
+ httpServer: http.Server;
47
+ /** The port the broker is listening on. */
48
+ port: number;
49
+ /** Stop the broker and release the port. */
50
+ close: () => Promise<void>;
51
+ }
52
+ /**
53
+ * Create and start a headless Buoy sync broker: an HTTP + Socket.IO server with
54
+ * the devtool sync protocol (v1) message routing attached. This is the same
55
+ * broker the desktop app embeds, extracted so a standalone process (the MCP
56
+ * server) can host one when no desktop app is running.
57
+ *
58
+ * Connect-or-spawn pattern: a client should first try to connect to a running
59
+ * broker on the port; only if that fails should it call `createBroker` to host
60
+ * its own. An `EADDRINUSE` from {@link CreateBrokerOptions.onError} means the
61
+ * port was taken between the probe and the spawn — fall back to connecting.
62
+ */
63
+ declare function createBroker(options?: CreateBrokerOptions): BrokerHandle;
64
+
65
+ /**
66
+ * DEVTOOL SYNC PROTOCOL (v1) — server-side wire contract.
67
+ *
68
+ * Canonical source of truth for these shapes is `@buoy-gg/external-sync`
69
+ * (packages/external-sync/src/types.ts). They are reproduced here so the
70
+ * broker stays a self-contained Node package with no React Native / client
71
+ * dependency (external-sync pulls in `socket.io-client` + RN). The shapes are
72
+ * structurally compatible — keep them in sync when the protocol version bumps.
73
+ *
74
+ * Topology: devices are Socket.IO *clients*; this broker hosts the Socket.IO
75
+ * *server* and routes messages between devices and one or more "Dashboard"
76
+ * clients (the desktop app today, the MCP server tomorrow).
77
+ */
78
+ /** Version of the envelope/event contract itself (not tool payloads). */
79
+ declare const SYNC_PROTOCOL_VERSION = 1;
80
+ /** What a sync payload represents. "events"/"diff" reserved for future use. */
81
+ type SyncKind = "snapshot";
82
+ /** Announces available tools + actions on connect and when tools change. */
83
+ interface DeviceCapabilitiesMessage {
84
+ persistentDeviceId: string;
85
+ protocolVersion: number;
86
+ tools: ToolCapability[];
87
+ }
88
+ interface ToolCapability {
89
+ toolId: string;
90
+ version: number;
91
+ actions: string[];
92
+ }
93
+ /** Carries a tool's state to the dashboard. */
94
+ interface DevToolSyncMessage {
95
+ toolId: string;
96
+ persistentDeviceId: string;
97
+ timestamp: number;
98
+ /** Tool payload version (ToolSyncAdapter.version). */
99
+ version: number;
100
+ kind: SyncKind;
101
+ payload: unknown;
102
+ }
103
+ /** Response to a `devtool-action` request. */
104
+ interface DevToolActionResultMessage {
105
+ requestId: string;
106
+ toolId: string;
107
+ action: string;
108
+ persistentDeviceId: string;
109
+ timestamp: number;
110
+ ok: boolean;
111
+ /** Present when ok. JSON-serializable value returned by the action. */
112
+ result?: unknown;
113
+ /** Present when not ok. */
114
+ error?: string;
115
+ }
116
+ /** Server-consolidated signal that a tool gained/lost its audience. */
117
+ interface DevToolWatchMessage {
118
+ toolId: string;
119
+ targetDeviceId: string;
120
+ watching: boolean;
121
+ }
122
+ /** Explicit request for a tool's current state (works even while unwatched). */
123
+ interface DevToolRequestMessage {
124
+ toolId: string;
125
+ targetDeviceId: string;
126
+ }
127
+ /** Invokes a registered action on the device. */
128
+ interface DevToolActionMessage {
129
+ requestId: string;
130
+ toolId: string;
131
+ targetDeviceId: string;
132
+ action: string;
133
+ params?: unknown;
134
+ }
135
+ /**
136
+ * A connected client (device or dashboard) as tracked by the broker.
137
+ * Mirrors the desktop dashboard's `User` type; the dashboard UI carries a few
138
+ * extra presentational fields, but the broker only needs these.
139
+ */
140
+ interface User {
141
+ id: string;
142
+ deviceName: string;
143
+ /**
144
+ * Persisted device ID. Optional because dashboard clients (the desktop app /
145
+ * MCP server) connect with `deviceName: "Dashboard"` and no deviceId — only
146
+ * real devices carry one.
147
+ */
148
+ deviceId?: string;
149
+ platform?: string;
150
+ isConnected?: boolean;
151
+ extraDeviceInfo?: string;
152
+ envVariables?: string;
153
+ /**
154
+ * Per-socket-instance identifier the device sends in its handshake. Stays
155
+ * constant across socket.io reconnects of the SAME client, but changes when
156
+ * the app creates a fresh socket (remount / Fast Refresh / duplicate mount).
157
+ */
158
+ connectionNonce?: string;
159
+ /** The socket.io reason for this client's most recent disconnect. */
160
+ lastDisconnectReason?: string;
161
+ /** Whether the device announced an active Buoy Pro license. */
162
+ isPro?: boolean;
163
+ /** The device's Buoy Pro license key, if any. Local dev connection only. */
164
+ licenseKey?: string;
165
+ }
166
+
167
+ export { type BrokerHandle, type CreateBrokerOptions, DEFAULT_BROKER_PORT, type DevToolActionMessage, type DevToolActionResultMessage, type DevToolRequestMessage, type DevToolSyncMessage, type DevToolWatchMessage, type DeviceCapabilitiesMessage, SOCKET_CONFIG, SYNC_PROTOCOL_VERSION, type SyncKind, type ToolCapability, type User, createBroker, socketHandle };
@@ -0,0 +1,167 @@
1
+ import { Server, ServerOptions } from 'socket.io';
2
+ import http from 'http';
3
+
4
+ type DefaultEventsMap = Record<string, (...args: any[]) => void>;
5
+ interface Props {
6
+ io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
7
+ }
8
+ /**
9
+ * Server-side socket handler that manages:
10
+ * - Communication between devices and dashboard
11
+ * - User connection tracking
12
+ * - Query state synchronization
13
+ * - Action forwarding
14
+ */
15
+ declare function socketHandle({ io }: Props): void;
16
+
17
+ /** Default port the Buoy sync broker listens on. */
18
+ declare const DEFAULT_BROKER_PORT = 42831;
19
+ /**
20
+ * Socket.IO server config shared by every broker host (the desktop app and the
21
+ * standalone/MCP host). Mirrors the desktop's historical config.
22
+ */
23
+ declare const SOCKET_CONFIG: Partial<ServerOptions>;
24
+ interface CreateBrokerOptions {
25
+ /** Port to listen on. @default DEFAULT_BROKER_PORT (42831) */
26
+ port?: number;
27
+ /**
28
+ * Extra Socket.IO server options merged over {@link SOCKET_CONFIG}. Use to
29
+ * override CORS, transports, etc. for a specific host.
30
+ */
31
+ socketOptions?: Partial<ServerOptions>;
32
+ /** Called once the broker is listening. */
33
+ onListening?: (port: number) => void;
34
+ /**
35
+ * Called on a server error. `EADDRINUSE` means another broker (e.g. the
36
+ * desktop app) already owns the port — the caller should connect to it as a
37
+ * client instead of hosting its own. Headless on purpose: no UI/dialog here,
38
+ * unlike the Electron app which surfaces a native dialog.
39
+ */
40
+ onError?: (error: NodeJS.ErrnoException) => void;
41
+ }
42
+ interface BrokerHandle {
43
+ /** The underlying Socket.IO server. */
44
+ io: Server;
45
+ /** The underlying HTTP server. */
46
+ httpServer: http.Server;
47
+ /** The port the broker is listening on. */
48
+ port: number;
49
+ /** Stop the broker and release the port. */
50
+ close: () => Promise<void>;
51
+ }
52
+ /**
53
+ * Create and start a headless Buoy sync broker: an HTTP + Socket.IO server with
54
+ * the devtool sync protocol (v1) message routing attached. This is the same
55
+ * broker the desktop app embeds, extracted so a standalone process (the MCP
56
+ * server) can host one when no desktop app is running.
57
+ *
58
+ * Connect-or-spawn pattern: a client should first try to connect to a running
59
+ * broker on the port; only if that fails should it call `createBroker` to host
60
+ * its own. An `EADDRINUSE` from {@link CreateBrokerOptions.onError} means the
61
+ * port was taken between the probe and the spawn — fall back to connecting.
62
+ */
63
+ declare function createBroker(options?: CreateBrokerOptions): BrokerHandle;
64
+
65
+ /**
66
+ * DEVTOOL SYNC PROTOCOL (v1) — server-side wire contract.
67
+ *
68
+ * Canonical source of truth for these shapes is `@buoy-gg/external-sync`
69
+ * (packages/external-sync/src/types.ts). They are reproduced here so the
70
+ * broker stays a self-contained Node package with no React Native / client
71
+ * dependency (external-sync pulls in `socket.io-client` + RN). The shapes are
72
+ * structurally compatible — keep them in sync when the protocol version bumps.
73
+ *
74
+ * Topology: devices are Socket.IO *clients*; this broker hosts the Socket.IO
75
+ * *server* and routes messages between devices and one or more "Dashboard"
76
+ * clients (the desktop app today, the MCP server tomorrow).
77
+ */
78
+ /** Version of the envelope/event contract itself (not tool payloads). */
79
+ declare const SYNC_PROTOCOL_VERSION = 1;
80
+ /** What a sync payload represents. "events"/"diff" reserved for future use. */
81
+ type SyncKind = "snapshot";
82
+ /** Announces available tools + actions on connect and when tools change. */
83
+ interface DeviceCapabilitiesMessage {
84
+ persistentDeviceId: string;
85
+ protocolVersion: number;
86
+ tools: ToolCapability[];
87
+ }
88
+ interface ToolCapability {
89
+ toolId: string;
90
+ version: number;
91
+ actions: string[];
92
+ }
93
+ /** Carries a tool's state to the dashboard. */
94
+ interface DevToolSyncMessage {
95
+ toolId: string;
96
+ persistentDeviceId: string;
97
+ timestamp: number;
98
+ /** Tool payload version (ToolSyncAdapter.version). */
99
+ version: number;
100
+ kind: SyncKind;
101
+ payload: unknown;
102
+ }
103
+ /** Response to a `devtool-action` request. */
104
+ interface DevToolActionResultMessage {
105
+ requestId: string;
106
+ toolId: string;
107
+ action: string;
108
+ persistentDeviceId: string;
109
+ timestamp: number;
110
+ ok: boolean;
111
+ /** Present when ok. JSON-serializable value returned by the action. */
112
+ result?: unknown;
113
+ /** Present when not ok. */
114
+ error?: string;
115
+ }
116
+ /** Server-consolidated signal that a tool gained/lost its audience. */
117
+ interface DevToolWatchMessage {
118
+ toolId: string;
119
+ targetDeviceId: string;
120
+ watching: boolean;
121
+ }
122
+ /** Explicit request for a tool's current state (works even while unwatched). */
123
+ interface DevToolRequestMessage {
124
+ toolId: string;
125
+ targetDeviceId: string;
126
+ }
127
+ /** Invokes a registered action on the device. */
128
+ interface DevToolActionMessage {
129
+ requestId: string;
130
+ toolId: string;
131
+ targetDeviceId: string;
132
+ action: string;
133
+ params?: unknown;
134
+ }
135
+ /**
136
+ * A connected client (device or dashboard) as tracked by the broker.
137
+ * Mirrors the desktop dashboard's `User` type; the dashboard UI carries a few
138
+ * extra presentational fields, but the broker only needs these.
139
+ */
140
+ interface User {
141
+ id: string;
142
+ deviceName: string;
143
+ /**
144
+ * Persisted device ID. Optional because dashboard clients (the desktop app /
145
+ * MCP server) connect with `deviceName: "Dashboard"` and no deviceId — only
146
+ * real devices carry one.
147
+ */
148
+ deviceId?: string;
149
+ platform?: string;
150
+ isConnected?: boolean;
151
+ extraDeviceInfo?: string;
152
+ envVariables?: string;
153
+ /**
154
+ * Per-socket-instance identifier the device sends in its handshake. Stays
155
+ * constant across socket.io reconnects of the SAME client, but changes when
156
+ * the app creates a fresh socket (remount / Fast Refresh / duplicate mount).
157
+ */
158
+ connectionNonce?: string;
159
+ /** The socket.io reason for this client's most recent disconnect. */
160
+ lastDisconnectReason?: string;
161
+ /** Whether the device announced an active Buoy Pro license. */
162
+ isPro?: boolean;
163
+ /** The device's Buoy Pro license key, if any. Local dev connection only. */
164
+ licenseKey?: string;
165
+ }
166
+
167
+ export { type BrokerHandle, type CreateBrokerOptions, DEFAULT_BROKER_PORT, type DevToolActionMessage, type DevToolActionResultMessage, type DevToolRequestMessage, type DevToolSyncMessage, type DevToolWatchMessage, type DeviceCapabilitiesMessage, SOCKET_CONFIG, SYNC_PROTOCOL_VERSION, type SyncKind, type ToolCapability, type User, createBroker, socketHandle };