@buoy-gg/sync-broker 7.0.43 → 7.0.44
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/dist/index.d.mts +78 -2
- package/dist/index.d.ts +78 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
1
|
import { Server, ServerOptions } from 'socket.io';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How dashboards prove themselves to this broker. Bump it whenever a broker
|
|
6
|
+
* change would refuse dashboards built before it, and set the same number in
|
|
7
|
+
* packages/mcp/package.json (`buoy.dashboardAuthVersion`). The release scripts
|
|
8
|
+
* (scripts/check-desktop-mcp-sync.mjs) then publish the MCP before Desktop.
|
|
9
|
+
* 0 was the old broker that trusted the name "Dashboard"; 1 is the token.
|
|
10
|
+
*/
|
|
11
|
+
declare const DASHBOARD_AUTH_VERSION = 1;
|
|
12
|
+
/** Rejection sent to a client that calls itself "Dashboard" without a valid token. */
|
|
13
|
+
declare const DASHBOARD_TOKEN_REQUIRED = "buoy:dashboard-token-required: this broker only accepts dashboards that present its token. Update Buoy Desktop and @buoy-gg/mcp.";
|
|
14
|
+
/** Rejection sent to a browser page whose origin wasn't approved. */
|
|
15
|
+
declare const ORIGIN_NOT_ALLOWED = "buoy:origin-not-allowed: this site isn't allowed to connect to Buoy. Approve it in Buoy Desktop.";
|
|
16
|
+
/** A fresh dashboard token: 32 random bytes, hex encoded. */
|
|
17
|
+
declare function createDashboardToken(): string;
|
|
18
|
+
/** Constant-time token check. Anything that isn't a non-empty string fails. */
|
|
19
|
+
declare function tokenMatches(expected: string, provided: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True for origins that can only belong to this machine or the local network:
|
|
22
|
+
* loopback, private LAN addresses, `.localhost`/`.local`/`.test` names, desktop
|
|
23
|
+
* and hybrid app schemes (`file:`, `tauri:`, `capacitor:`, `ionic:`).
|
|
24
|
+
*
|
|
25
|
+
* `null` is NOT local: a sandboxed iframe on any website sends `Origin: null`.
|
|
26
|
+
*/
|
|
27
|
+
declare function isLocalOrigin(origin: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* How the broker treats browser pages from origins that aren't local, such as
|
|
30
|
+
* an admin's production site. Without a policy, those origins are refused.
|
|
31
|
+
*/
|
|
32
|
+
interface OriginPolicy {
|
|
33
|
+
/** The owner's remembered answer for this origin, or undefined if never asked. */
|
|
34
|
+
remembered(origin: string): boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Ask the owner (Buoy Desktop shows a prompt). Resolve true to allow. Hosts
|
|
37
|
+
* with no one to ask (the standalone MCP broker) leave this out.
|
|
38
|
+
*/
|
|
39
|
+
ask?(origin: string): Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
|
|
4
42
|
type DefaultEventsMap = Record<string, (...args: any[]) => void>;
|
|
43
|
+
|
|
5
44
|
interface Props {
|
|
6
45
|
canUseDesktop?: () => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The secret a client must present (`auth.dashboardToken`) to be treated as
|
|
48
|
+
* a dashboard. Every other client is a device. See brokerAuth.ts.
|
|
49
|
+
*/
|
|
50
|
+
dashboardToken: string;
|
|
51
|
+
/** Decides browser pages from non-local origins. Absent = refuse them. */
|
|
52
|
+
originPolicy?: OriginPolicy;
|
|
7
53
|
io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
|
|
8
54
|
}
|
|
9
55
|
/**
|
|
@@ -13,7 +59,7 @@ interface Props {
|
|
|
13
59
|
* - Query state synchronization
|
|
14
60
|
* - Action forwarding
|
|
15
61
|
*/
|
|
16
|
-
declare function socketHandle({ io, canUseDesktop }: Props): {
|
|
62
|
+
declare function socketHandle({ io, canUseDesktop, dashboardToken, originPolicy, }: Props): {
|
|
17
63
|
refreshAccess(): void;
|
|
18
64
|
};
|
|
19
65
|
|
|
@@ -29,6 +75,19 @@ interface CreateBrokerOptions {
|
|
|
29
75
|
port?: number;
|
|
30
76
|
/** The host must supply its independently verified account admission. */
|
|
31
77
|
canUseDesktop?: () => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Token dashboards must present. Default: a fresh random one, returned on
|
|
80
|
+
* the handle. The host is responsible for writing it where its dashboards
|
|
81
|
+
* can read it (see brokerToken.ts).
|
|
82
|
+
*/
|
|
83
|
+
dashboardToken?: string;
|
|
84
|
+
/** Decides browser pages from non-local origins. Default: refuse them. */
|
|
85
|
+
originPolicy?: OriginPolicy;
|
|
86
|
+
/**
|
|
87
|
+
* Address to listen on. Default: every interface, so phones on the same
|
|
88
|
+
* Wi-Fi can connect. Pass "127.0.0.1" to accept this machine only.
|
|
89
|
+
*/
|
|
90
|
+
host?: string;
|
|
32
91
|
/**
|
|
33
92
|
* Extra Socket.IO server options merged over {@link SOCKET_CONFIG}. Use to
|
|
34
93
|
* override CORS, transports, etc. for a specific host.
|
|
@@ -51,6 +110,8 @@ interface BrokerHandle {
|
|
|
51
110
|
httpServer: http.Server;
|
|
52
111
|
/** The port the broker is listening on. */
|
|
53
112
|
port: number;
|
|
113
|
+
/** The token dashboards must present to this broker. */
|
|
114
|
+
dashboardToken: string;
|
|
54
115
|
/** Stop the broker and release the port. */
|
|
55
116
|
close: () => Promise<void>;
|
|
56
117
|
refreshAccess: () => void;
|
|
@@ -225,4 +286,19 @@ interface User {
|
|
|
225
286
|
collision?: boolean;
|
|
226
287
|
}
|
|
227
288
|
|
|
228
|
-
|
|
289
|
+
/** `~/.buoy` on macOS and Linux, `%APPDATA%\Buoy` on Windows. */
|
|
290
|
+
declare function buoyConfigDir(): string;
|
|
291
|
+
/**
|
|
292
|
+
* One file per port, so a test broker on another port (Desktop e2e, the MCP's
|
|
293
|
+
* own tests) never overwrites the token of the broker you actually use.
|
|
294
|
+
*/
|
|
295
|
+
declare function brokerTokenPath(port?: number): string;
|
|
296
|
+
/**
|
|
297
|
+
* Write the token, readable by this user only. Written to a temp file and
|
|
298
|
+
* renamed so a reader never sees a half-written token.
|
|
299
|
+
*/
|
|
300
|
+
declare function writeBrokerToken(token: string, file?: string): void;
|
|
301
|
+
/** The current token, or null when no broker has written one. */
|
|
302
|
+
declare function readBrokerToken(file?: string): string | null;
|
|
303
|
+
|
|
304
|
+
export { type BrokerHandle, type CreateBrokerOptions, DASHBOARD_AUTH_VERSION, DASHBOARD_TOKEN_REQUIRED, DEFAULT_BROKER_PORT, type DevToolActionMessage, type DevToolActionResultMessage, type DevToolRequestMessage, type DevToolSyncMessage, type DevToolWatchMessage, type DeviceCapabilitiesMessage, ORIGIN_NOT_ALLOWED, type OriginPolicy, SOCKET_CONFIG, SYNC_PROTOCOL_VERSION, type SyncKind, type ToolCapability, type User, brokerTokenPath, buoyConfigDir, createBroker, createDashboardToken, isLocalOrigin, readBrokerToken, socketHandle, tokenMatches, writeBrokerToken };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
1
|
import { Server, ServerOptions } from 'socket.io';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How dashboards prove themselves to this broker. Bump it whenever a broker
|
|
6
|
+
* change would refuse dashboards built before it, and set the same number in
|
|
7
|
+
* packages/mcp/package.json (`buoy.dashboardAuthVersion`). The release scripts
|
|
8
|
+
* (scripts/check-desktop-mcp-sync.mjs) then publish the MCP before Desktop.
|
|
9
|
+
* 0 was the old broker that trusted the name "Dashboard"; 1 is the token.
|
|
10
|
+
*/
|
|
11
|
+
declare const DASHBOARD_AUTH_VERSION = 1;
|
|
12
|
+
/** Rejection sent to a client that calls itself "Dashboard" without a valid token. */
|
|
13
|
+
declare const DASHBOARD_TOKEN_REQUIRED = "buoy:dashboard-token-required: this broker only accepts dashboards that present its token. Update Buoy Desktop and @buoy-gg/mcp.";
|
|
14
|
+
/** Rejection sent to a browser page whose origin wasn't approved. */
|
|
15
|
+
declare const ORIGIN_NOT_ALLOWED = "buoy:origin-not-allowed: this site isn't allowed to connect to Buoy. Approve it in Buoy Desktop.";
|
|
16
|
+
/** A fresh dashboard token: 32 random bytes, hex encoded. */
|
|
17
|
+
declare function createDashboardToken(): string;
|
|
18
|
+
/** Constant-time token check. Anything that isn't a non-empty string fails. */
|
|
19
|
+
declare function tokenMatches(expected: string, provided: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True for origins that can only belong to this machine or the local network:
|
|
22
|
+
* loopback, private LAN addresses, `.localhost`/`.local`/`.test` names, desktop
|
|
23
|
+
* and hybrid app schemes (`file:`, `tauri:`, `capacitor:`, `ionic:`).
|
|
24
|
+
*
|
|
25
|
+
* `null` is NOT local: a sandboxed iframe on any website sends `Origin: null`.
|
|
26
|
+
*/
|
|
27
|
+
declare function isLocalOrigin(origin: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* How the broker treats browser pages from origins that aren't local, such as
|
|
30
|
+
* an admin's production site. Without a policy, those origins are refused.
|
|
31
|
+
*/
|
|
32
|
+
interface OriginPolicy {
|
|
33
|
+
/** The owner's remembered answer for this origin, or undefined if never asked. */
|
|
34
|
+
remembered(origin: string): boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Ask the owner (Buoy Desktop shows a prompt). Resolve true to allow. Hosts
|
|
37
|
+
* with no one to ask (the standalone MCP broker) leave this out.
|
|
38
|
+
*/
|
|
39
|
+
ask?(origin: string): Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
|
|
4
42
|
type DefaultEventsMap = Record<string, (...args: any[]) => void>;
|
|
43
|
+
|
|
5
44
|
interface Props {
|
|
6
45
|
canUseDesktop?: () => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The secret a client must present (`auth.dashboardToken`) to be treated as
|
|
48
|
+
* a dashboard. Every other client is a device. See brokerAuth.ts.
|
|
49
|
+
*/
|
|
50
|
+
dashboardToken: string;
|
|
51
|
+
/** Decides browser pages from non-local origins. Absent = refuse them. */
|
|
52
|
+
originPolicy?: OriginPolicy;
|
|
7
53
|
io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
|
|
8
54
|
}
|
|
9
55
|
/**
|
|
@@ -13,7 +59,7 @@ interface Props {
|
|
|
13
59
|
* - Query state synchronization
|
|
14
60
|
* - Action forwarding
|
|
15
61
|
*/
|
|
16
|
-
declare function socketHandle({ io, canUseDesktop }: Props): {
|
|
62
|
+
declare function socketHandle({ io, canUseDesktop, dashboardToken, originPolicy, }: Props): {
|
|
17
63
|
refreshAccess(): void;
|
|
18
64
|
};
|
|
19
65
|
|
|
@@ -29,6 +75,19 @@ interface CreateBrokerOptions {
|
|
|
29
75
|
port?: number;
|
|
30
76
|
/** The host must supply its independently verified account admission. */
|
|
31
77
|
canUseDesktop?: () => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Token dashboards must present. Default: a fresh random one, returned on
|
|
80
|
+
* the handle. The host is responsible for writing it where its dashboards
|
|
81
|
+
* can read it (see brokerToken.ts).
|
|
82
|
+
*/
|
|
83
|
+
dashboardToken?: string;
|
|
84
|
+
/** Decides browser pages from non-local origins. Default: refuse them. */
|
|
85
|
+
originPolicy?: OriginPolicy;
|
|
86
|
+
/**
|
|
87
|
+
* Address to listen on. Default: every interface, so phones on the same
|
|
88
|
+
* Wi-Fi can connect. Pass "127.0.0.1" to accept this machine only.
|
|
89
|
+
*/
|
|
90
|
+
host?: string;
|
|
32
91
|
/**
|
|
33
92
|
* Extra Socket.IO server options merged over {@link SOCKET_CONFIG}. Use to
|
|
34
93
|
* override CORS, transports, etc. for a specific host.
|
|
@@ -51,6 +110,8 @@ interface BrokerHandle {
|
|
|
51
110
|
httpServer: http.Server;
|
|
52
111
|
/** The port the broker is listening on. */
|
|
53
112
|
port: number;
|
|
113
|
+
/** The token dashboards must present to this broker. */
|
|
114
|
+
dashboardToken: string;
|
|
54
115
|
/** Stop the broker and release the port. */
|
|
55
116
|
close: () => Promise<void>;
|
|
56
117
|
refreshAccess: () => void;
|
|
@@ -225,4 +286,19 @@ interface User {
|
|
|
225
286
|
collision?: boolean;
|
|
226
287
|
}
|
|
227
288
|
|
|
228
|
-
|
|
289
|
+
/** `~/.buoy` on macOS and Linux, `%APPDATA%\Buoy` on Windows. */
|
|
290
|
+
declare function buoyConfigDir(): string;
|
|
291
|
+
/**
|
|
292
|
+
* One file per port, so a test broker on another port (Desktop e2e, the MCP's
|
|
293
|
+
* own tests) never overwrites the token of the broker you actually use.
|
|
294
|
+
*/
|
|
295
|
+
declare function brokerTokenPath(port?: number): string;
|
|
296
|
+
/**
|
|
297
|
+
* Write the token, readable by this user only. Written to a temp file and
|
|
298
|
+
* renamed so a reader never sees a half-written token.
|
|
299
|
+
*/
|
|
300
|
+
declare function writeBrokerToken(token: string, file?: string): void;
|
|
301
|
+
/** The current token, or null when no broker has written one. */
|
|
302
|
+
declare function readBrokerToken(file?: string): string | null;
|
|
303
|
+
|
|
304
|
+
export { type BrokerHandle, type CreateBrokerOptions, DASHBOARD_AUTH_VERSION, DASHBOARD_TOKEN_REQUIRED, DEFAULT_BROKER_PORT, type DevToolActionMessage, type DevToolActionResultMessage, type DevToolRequestMessage, type DevToolSyncMessage, type DevToolWatchMessage, type DeviceCapabilitiesMessage, ORIGIN_NOT_ALLOWED, type OriginPolicy, SOCKET_CONFIG, SYNC_PROTOCOL_VERSION, type SyncKind, type ToolCapability, type User, brokerTokenPath, buoyConfigDir, createBroker, createDashboardToken, isLocalOrigin, readBrokerToken, socketHandle, tokenMatches, writeBrokerToken };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(i,r)=>{for(var d in r)__defProp(i,d,{get:r[d],enumerable:true})};var __copyProps=(i,r,d,c)=>{if(r&&typeof r==="object"||typeof r==="function"){for(let s of __getOwnPropNames(r))if(!__hasOwnProp.call(i,s)&&s!==d)__defProp(i,s,{get:()=>r[s],enumerable:!(c=__getOwnPropDesc(r,s))||c.enumerable})}return i};var __toESM=(i,r,d)=>(d=i!=null?__create(__getProtoOf(i)):{},__copyProps(r||!i||!i.__esModule?__defProp(d,"default",{value:i,enumerable:true}):d,i));var __toCommonJS=i=>__copyProps(__defProp({},"__esModule",{value:true}),i);var index_exports={};__export(index_exports,{DEFAULT_BROKER_PORT:()=>DEFAULT_BROKER_PORT,SOCKET_CONFIG:()=>SOCKET_CONFIG,SYNC_PROTOCOL_VERSION:()=>SYNC_PROTOCOL_VERSION,createBroker:()=>createBroker,socketHandle:()=>socketHandle});module.exports=__toCommonJS(index_exports);var SYNC_PROTOCOL_VERSION=1;function tierFromLicensePayload(i){if(i?.tier==="pro"||i?.tier==="free")return i.tier;if(i?.tier==="locked"||i?.tier==="anonymous")return"locked";return i?.isPro==="1"||i?.isPro===true?"pro":"locked"}var toWatchKey=(i,r)=>JSON.stringify([i,r]);var fromWatchKey=i=>JSON.parse(i);var WatchRegistry=class{constructor(){this.byDashboard=new Map;this.streamCounts=new Map;this.captureCounts=new Map}counts(i){return i==="stream"?this.streamCounts:this.captureCounts}bump(i,r,d){const c=this.counts(i);const s=(c.get(r)??0)+d;if(s>0)c.set(r,s);else c.delete(r)}effective(i){if((this.streamCounts.get(i)??0)>0)return"stream";if((this.captureCounts.get(i)??0)>0)return"capture";return null}activeKeys(){return[...new Set([...this.streamCounts.keys(),...this.captureCounts.keys()])]}watch(i,r,d){let c=this.byDashboard.get(i);if(!c){c=new Map;this.byDashboard.set(i,c)}const s=c.get(r);if(s!==d){if(s)this.bump(s,r,-1);this.bump(d,r,1);c.set(r,d)}return this.effective(r)}unwatch(i,r){const d=this.byDashboard.get(i);const c=d?.get(r);if(!d||!c)return{changed:false,mode:this.effective(r)};const s=this.effective(r);d.delete(r);this.bump(c,r,-1);const b=this.effective(r);return{changed:b!==s,mode:b}}releaseDashboard(i){const r=this.byDashboard.get(i);if(!r)return[];this.byDashboard.delete(i);const d=[];for(const[c,s]of r){const b=this.effective(c);this.bump(s,c,-1);const I=this.effective(c);if(I!==b)d.push({key:c,mode:I})}return d}};var BROKER_LOG_BUFFER_MAX=200;var MAX_OFFLINE_AGE_MS=24*60*60*1e3;function redactAccountKeys(i){if(!i)return void 0;try{const r=JSON.parse(i);if(!r||typeof r!=="object"||Array.isArray(r))return void 0;for(const d of["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"])delete r[d];return JSON.stringify(r)}catch{return void 0}}function socketHandle({io:i,canUseDesktop:r=()=>false}){const d="[SERVER]";let c=[];const s=[];let b=[];const I=new Map;const w=new Map;const N=new Map;const P=new Map;const y=new Map;let m=new WatchRegistry;const S=[];function O(e,t,n){console.log(`${d} ${t}`);const l={ts:Date.now(),level:e,message:t,data:n};S.push(l);if(S.length>BROKER_LOG_BUFFER_MAX){S.shift()}E("broker-log",l)}function K(e){const t=new Set;for(const[n,l]of I){if(l!==e)continue;const u=P.get(n);if(u)t.add(u)}return t.size}function C(){if(!r()){i.emit("all-devices-update",[]);return}for(const t of[...c,...s]){if(t.deviceName==="Dashboard"||!t.isConnected)continue;const n=w.get(t.id)??"locked";t.tier=n==="pro"?"pro":n==="free"?"free":"locked";t.isPro=n==="pro";if(n!=="free"&&n!=="pro"){delete t.envVariables;delete t.extraDeviceInfo}}const e=Date.now();for(const t of s){if(!t.deviceId)continue;const n=K(t.deviceId);const l=n>1;if(l&&!t.collision){O("warn",`${n} apps are registered as "${t.deviceId}" \u2014 actions sent to it land on an arbitrary one. Give each device its own externalSync.deviceId, or leave it unset for a per-install default.`,{deviceId:t.deviceId,instances:n})}else if(!l&&t.collision){O("info",`Device id collision cleared for "${t.deviceId}"`,{deviceId:t.deviceId})}t.collision=l}for(let t=s.length-1;t>=0;t--){const n=s[t];if(!n.isConnected&&n.lastSeenAt&&e-n.lastSeenAt>MAX_OFFLINE_AGE_MS){s.splice(t,1);if(n.deviceId){y.delete(n.deviceId)}}}i.emit("all-devices-update",s.filter(t=>t.deviceName!=="Dashboard").map(({licenseKey:t,...n})=>n))}function T(e,t){const n=c.find(h=>h.id===e);const l=I.get(e);I.delete(e);P.delete(e);w.delete(e);let u;if(l){for(const[h,D]of I){if(D===l)u=h}}if(l&&u){const h=c.findIndex(o=>o.deviceId===l);if(h!==-1&&c[h].id===e){c[h].id=u}const D=s.findIndex(o=>o.deviceId===l);if(D!==-1&&s[D].id===e){s[D].id=u}if(N.get(l)===e){N.set(l,u)}console.log(`${d} Client disconnected - ID: ${e}, Name: ${n?.deviceName||"Unknown"} - Device ${l} still connected via ${u}`)}else{c=c.filter(h=>h.id!==e);console.log(`${d} Client disconnected - ID: ${e}, Name: ${n?.deviceName||"Unknown"} - Removing user record`);if(l){const h=s.findIndex(D=>D.deviceId===l);if(h!==-1){s[h].isConnected=false;s[h].lastDisconnectReason=t;s[h].lastSeenAt=Date.now();N.delete(l)}}}const v=b.includes(e);b=b.filter(h=>h!==e);F(e);const p=Boolean(l&&u);O(v||p?"debug":"warn",p?`Socket closed for ${n?.deviceName||e} (${t??"unknown"}) \u2014 device still connected via another socket`:`Disconnected: ${n?.deviceName||e} (${t??"unknown"})`,{socketId:e,reason:t,wasDashboard:v,stillConnected:p});C();console.log(`${d} Updated users list: ${c.map(h=>h.deviceName).join(", ")}`)}function B({id:e,deviceName:t,deviceId:n,platform:l,extraDeviceInfo:u,envVariables:v,connectionNonce:p}){const h=t==="Dashboard";if(n){I.set(e,n);N.set(n,e);const a=c.findIndex($=>$.deviceId===n);const f=s.findIndex($=>$.deviceId===n);if(a!==-1){O("success",`Reconnected: ${t} (${l||"unknown platform"})`,{socketId:e,deviceId:n});c[a].id=e;if(l){c[a].platform=l}if(u){c[a].extraDeviceInfo=u}if(v){c[a].envVariables=v}c[a].isConnected=true;c[a].connectionNonce=p;if(f!==-1){s[f].id=e;s[f].isConnected=true;s[f].connectionNonce=p;s[f].lastSeenAt=Date.now();if(l){s[f].platform=l}if(u){s[f].extraDeviceInfo=u}if(v){s[f].envVariables=v}}else if(t!=="Dashboard"){s.push({id:e,deviceName:t,deviceId:n,platform:l,isConnected:true,extraDeviceInfo:u,envVariables:v,lastSeenAt:Date.now()})}C();console.log(`${d} Updated users list: ${c.map($=>$.deviceName).join(", ")}`);return}}const D=a=>{const f=a.match(/(\d+)$/);return f?parseInt(f[1],10):1};let o=0;c.forEach(a=>{if(a.deviceName.startsWith(t)){const f=D(a.deviceName);if(f>=o){o=f}}});if(o>0&&!h){const a=`${t} #${o+1}`;O("warn",`Duplicate device name "${t}" \u2014 renamed to "${a}"`,{socketId:e,deviceId:n});t=a}if(e){const a={id:e,deviceName:t,deviceId:n,platform:l,isConnected:true,extraDeviceInfo:u,envVariables:v,connectionNonce:p,lastSeenAt:Date.now()};c.push(a);if(t!=="Dashboard"&&n){const f=s.findIndex($=>$.deviceId===n);if(f!==-1){s[f]={...a}}else{s.push({...a})}}O(h?"debug":"info",`Connected: ${t}${l?` (${l})`:""}`,{socketId:e,deviceId:n});if(h&&e){b.push(e);console.log(`${d} Registered dashboard client with ID: ${e}`)}C();console.log(`${d} Updated device list: ${c.map(f=>f.deviceName).join(", ")}`)}}function M(e){return s.find(t=>t.deviceId===e)}function U(e,t){console.log(`${d} Message from ${t}: ${e}`);if(!r())return;i.emit("message",`Device ${t} sent message: ${e}`)}function E(e,t){if(!r())return;b=b.filter(n=>i.sockets.sockets.has(n));if(b.length===0){return}for(const n of b){i.sockets.sockets.get(n)?.emit(e,t)}}function x(e,t,n){if(!r())return;const l=v=>{const p=w.get(v.id);return p==="free"||p==="pro"};const u=M(e)?.deviceName;if(e==="No devices available"){console.log(`${d} Skipping ${n} - No devices available`);return}if(e==="All"){console.log(`${d} Broadcasting ${n} to all devices`);const v=c.filter(p=>p.deviceName!=="Dashboard");console.log(`${d} Sending to ${v.length} devices: ${v.map(p=>p.deviceName).join(", ")}`);v.filter(l).forEach(t)}else{const v=c.find(p=>p.deviceId===e);if(v&&l(v)){console.log(`${d} Sending to target device ${v.deviceName} (${v.deviceId}) targetDeviceId: ${e}`);t(v)}else{console.log(`${d} Target device not found - DeviceId: ${e}, DeviceName: ${u}`)}}}function A(e,t){const[n,l]=fromWatchKey(e);const u=t===null?{toolId:l,targetDeviceId:n,watching:false,consolidated:true}:{toolId:l,targetDeviceId:n,watching:true,stream:t==="stream",consolidated:true};x(n,v=>i.to(v.id).emit("devtool-watch",u),`devtool ${t===null?"unwatch":`watch(${t})`} ${l}`)}function L(e,t){const n=toWatchKey(t.targetDeviceId,t.toolId);if(t.watching){const v=t.stream===false?"capture":"stream";const p=m.watch(e,n,v);console.log(`${d} Dashboard ${e} watching "${t.toolId}" on ${t.targetDeviceId} (${v}, effective ${p})`);A(n,p);return}const{changed:l,mode:u}=m.unwatch(e,n);if(!l)return;console.log(u===null?`${d} Last dashboard stopped watching "${t.toolId}" on ${t.targetDeviceId}`:`${d} "${t.toolId}" on ${t.targetDeviceId} downgraded to ${u}`);A(n,u)}function F(e){for(const{key:t,mode:n}of m.releaseDashboard(e)){A(t,n)}}i.on("connection",e=>{const t=()=>e.emit("buoy:access",{protocol:1,ready:r()});t();e.on("buoy:access:request",t);e.use((o,a)=>{if(o[0]==="buoy:access:request"){a();return}if(!r()){t();return}const f=o[0]==="device-license";if(f){const g=o[1]??{};w.set(e.id,tierFromLicensePayload(g))}const $=w.get(e.id)==="free"||w.get(e.id)==="pro";if(f||b.includes(e.id)||$)a();else t()});const{deviceName:n,deviceId:l,platform:u,extraDeviceInfo:v,envVariables:p,connectionNonce:h,instanceId:D}=e.handshake.query;if(typeof D==="string"&&D){P.set(e.id,D)}console.log(`${d} New connection - ID: ${e.id}, Name: ${n||"Unknown Device"}${l?`, DeviceId: ${l}`:""}${u?`, Platform: ${u}`:""}`);B({id:e.id,deviceName:n||"Unknown Device Name",deviceId:l,platform:u,extraDeviceInfo:void 0,envVariables:void 0,connectionNonce:h});if(r()&&n==="Dashboard"){for(const o of y.values()){e.emit("devtool-capabilities",o)}for(const o of S){e.emit("broker-log",o)}}e.on("device-license",o=>{const a=tierFromLicensePayload(o??{});const f=a==="pro";w.set(e.id,a);const $=c.find(_=>_.id===e.id);if($){$.isPro=f;$.tier=a;delete $.licenseKey}const g=$?.deviceId||I.get(e.id);if(a!=="locked"){for(const _ of[...c,...s].filter(R=>R.id===e.id)){_.extraDeviceInfo=redactAccountKeys(v);_.envVariables=redactAccountKeys(p)}}if(g){if(a==="locked")y.delete(g);const _=s.find(R=>R.deviceId===g);if(_){_.isPro=f;_.tier=a;delete _.licenseKey}}O("debug",`device-license from ${$?.deviceName||e.id}: tier=${a}`,{socketId:e.id});C()});e.on("disconnect",o=>T(e.id,o));e.on("message",o=>{U(o,n||"Unknown")});e.on("devtool-capabilities",o=>{if(!o||!Array.isArray(o.tools)||o.persistentDeviceId!==I.get(e.id))return;console.log(`${d} Capabilities from ${o.persistentDeviceId}: ${o.tools.map(a=>`${a.toolId}@v${a.version}`).join(", ")}`);if(o.protocolVersion!==SYNC_PROTOCOL_VERSION){O("warn",`Protocol version mismatch from ${o.persistentDeviceId}: device speaks v${o.protocolVersion}, broker speaks v${SYNC_PROTOCOL_VERSION} \u2014 update @buoy-gg packages or Buoy Desktop`,{deviceId:o.persistentDeviceId})}y.set(o.persistentDeviceId,o);E("devtool-capabilities",o);for(const a of m.activeKeys()){const[f,$]=fromWatchKey(a);if(f!=="All"&&f!==o.persistentDeviceId){continue}const g=m.effective(a);if(!g)continue;e.emit("devtool-watch",{toolId:$,targetDeviceId:f,watching:true,stream:g==="stream",consolidated:true})}});e.on("devtool-sync",o=>{if(!o||o.persistentDeviceId!==I.get(e.id))return;E("devtool-sync",o)});e.on("devtool-action-result",o=>{if(!o||o.persistentDeviceId!==I.get(e.id))return;console.log(`${d} Action result from ${o.persistentDeviceId}: ${o.toolId}.${o.action} -> ${o.ok?"ok":`error: ${o.error}`}`);E("devtool-action-result",o)});e.on("devtool-watch",o=>{L(e.id,o)});e.on("request-devtool-state",o=>{x(o.targetDeviceId,a=>i.to(a.id).emit("request-devtool-state",o),`devtool state request ${o.toolId}`)});e.on("forget-device",o=>{const a=o?.deviceId;if(!b.includes(e.id))return;if(!a||N.has(a))return;const f=s.find(g=>g.deviceId===a);c=c.filter(g=>g.deviceId!==a);const $=s.findIndex(g=>g.deviceId===a);if($!==-1){s.splice($,1)}y.delete(a);for(const[g,_]of I){if(_===a){I.delete(g)}}O("info",`Forgot device: ${f?.deviceName??a}`,{deviceId:a});C()});e.on("devtool-action",o=>{console.log(`${d} Devtool action from dashboard: ${o.toolId}.${o.action} -> ${o.targetDeviceId}`);x(o.targetDeviceId,a=>i.to(a.id).emit("devtool-action",o),`devtool action ${o.toolId}.${o.action}`)})});return{refreshAccess(){const e=r();if(!e){w.clear();y.clear();S.length=0;m=new WatchRegistry;for(const t of[...c,...s]){delete t.licenseKey;delete t.envVariables;delete t.extraDeviceInfo}}i.emit("buoy:access",{protocol:1,ready:e});C()}}}var import_http=__toESM(require("http"));var import_socket=require("socket.io");var DEFAULT_BROKER_PORT=42831;var SOCKET_CONFIG={cors:{origin:"*",methods:["GET","POST"]},maxHttpBufferSize:1e8,transports:["websocket","polling"],pingTimeout:3e4,pingInterval:25e3};function createBroker(i={}){const r=i.port??DEFAULT_BROKER_PORT;const d=import_http.default.createServer((I,w)=>{w.writeHead(200,{"content-type":"text/plain"});w.end(`Buoy sync broker running on port ${r}`)});const c=new import_socket.Server(d,{...SOCKET_CONFIG,...i.socketOptions});const s=socketHandle({io:c,canUseDesktop:i.canUseDesktop});d.on("error",I=>{i.onError?.(I)});d.listen(r,()=>{i.onListening?.(r)});const b=()=>new Promise(I=>{c.close(()=>{d.close(()=>I())})});return{io:c,httpServer:d,port:r,close:b,refreshAccess:s.refreshAccess}}
|
|
1
|
+
"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(o,n)=>{for(var a in n)__defProp(o,a,{get:n[a],enumerable:true})};var __copyProps=(o,n,a,u)=>{if(n&&typeof n==="object"||typeof n==="function"){for(let d of __getOwnPropNames(n))if(!__hasOwnProp.call(o,d)&&d!==a)__defProp(o,d,{get:()=>n[d],enumerable:!(u=__getOwnPropDesc(n,d))||u.enumerable})}return o};var __toESM=(o,n,a)=>(a=o!=null?__create(__getProtoOf(o)):{},__copyProps(n||!o||!o.__esModule?__defProp(a,"default",{value:o,enumerable:true}):a,o));var __toCommonJS=o=>__copyProps(__defProp({},"__esModule",{value:true}),o);var index_exports={};__export(index_exports,{DASHBOARD_AUTH_VERSION:()=>DASHBOARD_AUTH_VERSION,DASHBOARD_TOKEN_REQUIRED:()=>DASHBOARD_TOKEN_REQUIRED,DEFAULT_BROKER_PORT:()=>DEFAULT_BROKER_PORT,ORIGIN_NOT_ALLOWED:()=>ORIGIN_NOT_ALLOWED,SOCKET_CONFIG:()=>SOCKET_CONFIG,SYNC_PROTOCOL_VERSION:()=>SYNC_PROTOCOL_VERSION,brokerTokenPath:()=>brokerTokenPath,buoyConfigDir:()=>buoyConfigDir,createBroker:()=>createBroker,createDashboardToken:()=>createDashboardToken,isLocalOrigin:()=>isLocalOrigin,readBrokerToken:()=>readBrokerToken,socketHandle:()=>socketHandle,tokenMatches:()=>tokenMatches,writeBrokerToken:()=>writeBrokerToken});module.exports=__toCommonJS(index_exports);var SYNC_PROTOCOL_VERSION=1;function tierFromLicensePayload(o){if(o?.tier==="pro"||o?.tier==="free")return o.tier;if(o?.tier==="locked"||o?.tier==="anonymous")return"locked";return o?.isPro==="1"||o?.isPro===true?"pro":"locked"}var toWatchKey=(o,n)=>JSON.stringify([o,n]);var fromWatchKey=o=>JSON.parse(o);var WatchRegistry=class{constructor(){this.byDashboard=new Map;this.streamCounts=new Map;this.captureCounts=new Map}counts(o){return o==="stream"?this.streamCounts:this.captureCounts}bump(o,n,a){const u=this.counts(o);const d=(u.get(n)??0)+a;if(d>0)u.set(n,d);else u.delete(n)}effective(o){if((this.streamCounts.get(o)??0)>0)return"stream";if((this.captureCounts.get(o)??0)>0)return"capture";return null}activeKeys(){return[...new Set([...this.streamCounts.keys(),...this.captureCounts.keys()])]}watch(o,n,a){let u=this.byDashboard.get(o);if(!u){u=new Map;this.byDashboard.set(o,u)}const d=u.get(n);if(d!==a){if(d)this.bump(d,n,-1);this.bump(a,n,1);u.set(n,a)}return this.effective(n)}unwatch(o,n){const a=this.byDashboard.get(o);const u=a?.get(n);if(!a||!u)return{changed:false,mode:this.effective(n)};const d=this.effective(n);a.delete(n);this.bump(u,n,-1);const h=this.effective(n);return{changed:h!==d,mode:h}}releaseDashboard(o){const n=this.byDashboard.get(o);if(!n)return[];this.byDashboard.delete(o);const a=[];for(const[u,d]of n){const h=this.effective(u);this.bump(d,u,-1);const f=this.effective(u);if(f!==h)a.push({key:u,mode:f})}return a}};var import_crypto=require("crypto");var DASHBOARD_AUTH_VERSION=1;var DASHBOARD_TOKEN_REQUIRED="buoy:dashboard-token-required: this broker only accepts dashboards that present its token. Update Buoy Desktop and @buoy-gg/mcp.";var ORIGIN_NOT_ALLOWED="buoy:origin-not-allowed: this site isn't allowed to connect to Buoy. Approve it in Buoy Desktop.";function createDashboardToken(){return(0,import_crypto.randomBytes)(32).toString("hex")}function tokenMatches(o,n){if(typeof n!=="string"||!o||!n)return false;const a=Buffer.from(o);const u=Buffer.from(n);return a.length===u.length&&(0,import_crypto.timingSafeEqual)(a,u)}function isPrivateIPv4(o){const n=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(o);if(!n)return false;const[a,u]=[Number(n[1]),Number(n[2])];return a===127||a===10||a===172&&u>=16&&u<=31||a===192&&u===168||a===169&&u===254}function isLocalHostname(o){if(o==="localhost"||o.endsWith(".localhost"))return true;if(o.endsWith(".local")||o.endsWith(".test"))return true;if(isPrivateIPv4(o))return true;if(o==="::1")return true;const n=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(o);if(n)return isPrivateIPv4(n[1]);return/^f[cd][0-9a-f]{0,2}:/.test(o)||/^fe[89ab][0-9a-f]?:/.test(o)}function isLocalOrigin(o){let n;try{n=new URL(o)}catch{return false}if(n.protocol==="file:")return true;const a=n.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(n.protocol==="tauri:"||n.protocol==="capacitor:"||n.protocol==="ionic:"){return a==="localhost"}if(n.protocol!=="http:"&&n.protocol!=="https:")return false;return isLocalHostname(a)}var ORIGIN_ASK_TIMEOUT_MS=12e4;var BROKER_LOG_BUFFER_MAX=200;var MAX_OFFLINE_AGE_MS=24*60*60*1e3;function redactAccountKeys(o){if(!o)return void 0;try{const n=JSON.parse(o);if(!n||typeof n!=="object"||Array.isArray(n))return void 0;for(const a of["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","NEXT_PUBLIC_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"])delete n[a];return JSON.stringify(n)}catch{return void 0}}function socketHandle({io:o,canUseDesktop:n=()=>false,dashboardToken:a,originPolicy:u}){const d="[SERVER]";let h=[];const f=[];let _=[];const $=new Map;const O=new Map;const A=new Map;const C=new Map;const E=new Map;let S=new WatchRegistry;const k=[];function y(e,t,i){console.log(`${d} ${t}`);const s={ts:Date.now(),level:e,message:t,data:i};k.push(s);if(k.length>BROKER_LOG_BUFFER_MAX){k.shift()}R("broker-log",s)}function M(e){const t=new Set;for(const[i,s]of $){if(s!==e)continue;const v=C.get(i);if(v)t.add(v)}return t.size}function T(){if(!n()){P("all-devices-update",[]);return}for(const t of[...h,...f]){if(t.deviceName==="Dashboard"||!t.isConnected)continue;const i=O.get(t.id)??"locked";t.tier=i==="pro"?"pro":i==="free"?"free":"locked";t.isPro=i==="pro";if(i!=="free"&&i!=="pro"){delete t.envVariables;delete t.extraDeviceInfo}}const e=Date.now();for(const t of f){if(!t.deviceId)continue;const i=M(t.deviceId);const s=i>1;if(s&&!t.collision){y("warn",`${i} apps are registered as "${t.deviceId}" \u2014 actions sent to it land on an arbitrary one. Give each device its own externalSync.deviceId, or leave it unset for a per-install default.`,{deviceId:t.deviceId,instances:i})}else if(!s&&t.collision){y("info",`Device id collision cleared for "${t.deviceId}"`,{deviceId:t.deviceId})}t.collision=s}for(let t=f.length-1;t>=0;t--){const i=f[t];if(!i.isConnected&&i.lastSeenAt&&e-i.lastSeenAt>MAX_OFFLINE_AGE_MS){f.splice(t,1);if(i.deviceId){E.delete(i.deviceId)}}}P("all-devices-update",f.filter(t=>t.deviceName!=="Dashboard").map(({licenseKey:t,...i})=>i))}function j(e,t){const i=h.find(p=>p.id===e);const s=$.get(e);$.delete(e);C.delete(e);O.delete(e);let v;if(s){for(const[p,g]of $){if(g===s)v=p}}if(s&&v){const p=h.findIndex(w=>w.deviceId===s);if(p!==-1&&h[p].id===e){h[p].id=v}const g=f.findIndex(w=>w.deviceId===s);if(g!==-1&&f[g].id===e){f[g].id=v}if(A.get(s)===e){A.set(s,v)}console.log(`${d} Client disconnected - ID: ${e}, Name: ${i?.deviceName||"Unknown"} - Device ${s} still connected via ${v}`)}else{h=h.filter(p=>p.id!==e);console.log(`${d} Client disconnected - ID: ${e}, Name: ${i?.deviceName||"Unknown"} - Removing user record`);if(s){const p=f.findIndex(g=>g.deviceId===s);if(p!==-1){f[p].isConnected=false;f[p].lastDisconnectReason=t;f[p].lastSeenAt=Date.now();A.delete(s)}}}const l=_.includes(e);_=_.filter(p=>p!==e);G(e);const I=Boolean(s&&v);y(l||I?"debug":"warn",I?`Socket closed for ${i?.deviceName||e} (${t??"unknown"}) \u2014 device still connected via another socket`:`Disconnected: ${i?.deviceName||e} (${t??"unknown"})`,{socketId:e,reason:t,wasDashboard:l,stillConnected:I});T();console.log(`${d} Updated users list: ${h.map(p=>p.deviceName).join(", ")}`)}function q({id:e,deviceName:t,deviceId:i,platform:s,extraDeviceInfo:v,envVariables:l,connectionNonce:I},p){if(i){$.set(e,i);A.set(i,e);const r=h.findIndex(b=>b.deviceId===i);const c=f.findIndex(b=>b.deviceId===i);if(r!==-1){y("success",`Reconnected: ${t} (${s||"unknown platform"})`,{socketId:e,deviceId:i});h[r].id=e;if(s){h[r].platform=s}if(v){h[r].extraDeviceInfo=v}if(l){h[r].envVariables=l}h[r].isConnected=true;h[r].connectionNonce=I;if(c!==-1){f[c].id=e;f[c].isConnected=true;f[c].connectionNonce=I;f[c].lastSeenAt=Date.now();if(s){f[c].platform=s}if(v){f[c].extraDeviceInfo=v}if(l){f[c].envVariables=l}}else if(t!=="Dashboard"){f.push({id:e,deviceName:t,deviceId:i,platform:s,isConnected:true,extraDeviceInfo:v,envVariables:l,lastSeenAt:Date.now()})}T();console.log(`${d} Updated users list: ${h.map(b=>b.deviceName).join(", ")}`);return}}const g=r=>{const c=r.match(/(\d+)$/);return c?parseInt(c[1],10):1};let w=0;h.forEach(r=>{if(r.deviceName.startsWith(t)){const c=g(r.deviceName);if(c>=w){w=c}}});if(w>0&&!p){const r=`${t} #${w+1}`;y("warn",`Duplicate device name "${t}" \u2014 renamed to "${r}"`,{socketId:e,deviceId:i});t=r}if(e){const r={id:e,deviceName:t,deviceId:i,platform:s,isConnected:true,extraDeviceInfo:v,envVariables:l,connectionNonce:I,lastSeenAt:Date.now()};h.push(r);if(t!=="Dashboard"&&i){const c=f.findIndex(b=>b.deviceId===i);if(c!==-1){f[c]={...r}}else{f.push({...r})}}y(p?"debug":"info",`Connected: ${t}${s?` (${s})`:""}`,{socketId:e,deviceId:i});if(p&&e){_.push(e);console.log(`${d} Registered dashboard client with ID: ${e}`)}T();console.log(`${d} Updated device list: ${h.map(c=>c.deviceName).join(", ")}`)}}function F(e){return f.find(t=>t.deviceId===e)}function H(e,t){console.log(`${d} Message from ${t}: ${e}`)}function R(e,t){if(!n())return;P(e,t)}function P(e,t){_=_.filter(i=>o.sockets.sockets.has(i));for(const i of _){o.sockets.sockets.get(i)?.emit(e,t)}}function B(e){return _.includes(e)}const U=new Map;function Y(e,t){let i=U.get(e);if(!i){let s;const v=new Promise(l=>{s=setTimeout(()=>l(false),ORIGIN_ASK_TIMEOUT_MS)});i=Promise.race([t(e).catch(()=>false),v]).finally(()=>{clearTimeout(s);U.delete(e)});U.set(e,i)}return i}o.use((e,t)=>{const{auth:i,query:s,headers:v}=e.handshake;if(tokenMatches(a,i?.dashboardToken)){t();return}if(s?.deviceName==="Dashboard"){y("warn","Refused a dashboard connection without the broker token (old Buoy Desktop or @buoy-gg/mcp?)",{address:e.handshake.address});t(new Error(DASHBOARD_TOKEN_REQUIRED));return}const l=typeof v?.origin==="string"?v.origin:void 0;if(!l||isLocalOrigin(l)){t();return}const I=u?.remembered(l);if(I===true){t();return}const p=()=>{y("warn",`Refused a connection from ${l}. Approve the site in Buoy Desktop to let it connect.`,{origin:l});t(new Error(ORIGIN_NOT_ALLOWED))};if(I===false||!u?.ask){p();return}void Y(l,u.ask).then(g=>g?t():p())});function L(e,t,i){if(!n())return;const s=l=>{const I=O.get(l.id);return I==="free"||I==="pro"};const v=F(e)?.deviceName;if(e==="No devices available"){console.log(`${d} Skipping ${i} - No devices available`);return}if(e==="All"){console.log(`${d} Broadcasting ${i} to all devices`);const l=h.filter(I=>I.deviceName!=="Dashboard");console.log(`${d} Sending to ${l.length} devices: ${l.map(I=>I.deviceName).join(", ")}`);l.filter(s).forEach(t)}else{const l=h.find(I=>I.deviceId===e);if(l&&s(l)){console.log(`${d} Sending to target device ${l.deviceName} (${l.deviceId}) targetDeviceId: ${e}`);t(l)}else{console.log(`${d} Target device not found - DeviceId: ${e}, DeviceName: ${v}`)}}}function x(e,t){const[i,s]=fromWatchKey(e);const v=t===null?{toolId:s,targetDeviceId:i,watching:false,consolidated:true}:{toolId:s,targetDeviceId:i,watching:true,stream:t==="stream",consolidated:true};L(i,l=>o.to(l.id).emit("devtool-watch",v),`devtool ${t===null?"unwatch":`watch(${t})`} ${s}`)}function W(e,t){const i=toWatchKey(t.targetDeviceId,t.toolId);if(t.watching){const l=t.stream===false?"capture":"stream";const I=S.watch(e,i,l);console.log(`${d} Dashboard ${e} watching "${t.toolId}" on ${t.targetDeviceId} (${l}, effective ${I})`);x(i,I);return}const{changed:s,mode:v}=S.unwatch(e,i);if(!s)return;console.log(v===null?`${d} Last dashboard stopped watching "${t.toolId}" on ${t.targetDeviceId}`:`${d} "${t.toolId}" on ${t.targetDeviceId} downgraded to ${v}`);x(i,v)}function G(e){for(const{key:t,mode:i}of S.releaseDashboard(e)){x(t,i)}}o.on("connection",e=>{const t=tokenMatches(a,e.handshake.auth?.dashboardToken);if(!t&&e.handshake.query?.deviceName==="Dashboard"){e.emit("buoy:auth-error",{reason:DASHBOARD_TOKEN_REQUIRED});e.disconnect(true);return}const i=()=>e.emit("buoy:access",{protocol:1,ready:n()});i();e.on("buoy:access:request",i);e.use((r,c)=>{if(r[0]==="buoy:access:request"){c();return}if(!n()){i();return}const b=r[0]==="device-license";if(b){const D=r[1]??{};O.set(e.id,tierFromLicensePayload(D))}const m=O.get(e.id)==="free"||O.get(e.id)==="pro";if(b||_.includes(e.id)||m)c();else i()});const{deviceName:s,deviceId:v,platform:l,extraDeviceInfo:I,envVariables:p,connectionNonce:g,instanceId:w}=e.handshake.query;if(typeof w==="string"&&w){C.set(e.id,w)}console.log(`${d} New connection - ID: ${e.id}, Name: ${s||"Unknown Device"}${v?`, DeviceId: ${v}`:""}${l?`, Platform: ${l}`:""}`);q({id:e.id,deviceName:s||"Unknown Device Name",deviceId:v,platform:l,extraDeviceInfo:void 0,envVariables:void 0,connectionNonce:g},t);if(n()&&t){for(const r of E.values()){e.emit("devtool-capabilities",r)}for(const r of k){e.emit("broker-log",r)}}e.on("device-license",r=>{const c=tierFromLicensePayload(r??{});const b=c==="pro";O.set(e.id,c);const m=h.find(N=>N.id===e.id);if(m){m.isPro=b;m.tier=c;delete m.licenseKey}const D=m?.deviceId||$.get(e.id);if(c!=="locked"){for(const N of[...h,...f].filter(K=>K.id===e.id)){N.extraDeviceInfo=redactAccountKeys(I);N.envVariables=redactAccountKeys(p)}}if(D){if(c==="locked")E.delete(D);const N=f.find(K=>K.deviceId===D);if(N){N.isPro=b;N.tier=c;delete N.licenseKey}}y("debug",`device-license from ${m?.deviceName||e.id}: tier=${c}`,{socketId:e.id});T()});e.on("disconnect",r=>j(e.id,r));e.on("message",r=>{H(r,s||"Unknown")});e.on("devtool-capabilities",r=>{if(!r||!Array.isArray(r.tools)||r.persistentDeviceId!==$.get(e.id))return;console.log(`${d} Capabilities from ${r.persistentDeviceId}: ${r.tools.map(c=>`${c.toolId}@v${c.version}`).join(", ")}`);if(r.protocolVersion!==SYNC_PROTOCOL_VERSION){y("warn",`Protocol version mismatch from ${r.persistentDeviceId}: device speaks v${r.protocolVersion}, broker speaks v${SYNC_PROTOCOL_VERSION} \u2014 update @buoy-gg packages or Buoy Desktop`,{deviceId:r.persistentDeviceId})}E.set(r.persistentDeviceId,r);R("devtool-capabilities",r);for(const c of S.activeKeys()){const[b,m]=fromWatchKey(c);if(b!=="All"&&b!==r.persistentDeviceId){continue}const D=S.effective(c);if(!D)continue;e.emit("devtool-watch",{toolId:m,targetDeviceId:b,watching:true,stream:D==="stream",consolidated:true})}});e.on("devtool-sync",r=>{if(!r||r.persistentDeviceId!==$.get(e.id))return;R("devtool-sync",r)});e.on("devtool-action-result",r=>{if(!r||r.persistentDeviceId!==$.get(e.id))return;console.log(`${d} Action result from ${r.persistentDeviceId}: ${r.toolId}.${r.action} -> ${r.ok?"ok":`error: ${r.error}`}`);R("devtool-action-result",r)});e.on("devtool-watch",r=>{if(!B(e.id))return;W(e.id,r)});e.on("request-devtool-state",r=>{if(!B(e.id))return;L(r.targetDeviceId,c=>o.to(c.id).emit("request-devtool-state",r),`devtool state request ${r.toolId}`)});e.on("forget-device",r=>{const c=r?.deviceId;if(!_.includes(e.id))return;if(!c||A.has(c))return;const b=f.find(D=>D.deviceId===c);h=h.filter(D=>D.deviceId!==c);const m=f.findIndex(D=>D.deviceId===c);if(m!==-1){f.splice(m,1)}E.delete(c);for(const[D,N]of $){if(N===c){$.delete(D)}}y("info",`Forgot device: ${b?.deviceName??c}`,{deviceId:c});T()});e.on("devtool-action",r=>{if(!B(e.id))return;console.log(`${d} Devtool action from dashboard: ${r.toolId}.${r.action} -> ${r.targetDeviceId}`);L(r.targetDeviceId,c=>o.to(c.id).emit("devtool-action",r),`devtool action ${r.toolId}.${r.action}`)})});return{refreshAccess(){const e=n();if(!e){O.clear();E.clear();k.length=0;S=new WatchRegistry;for(const t of[...h,...f]){delete t.licenseKey;delete t.envVariables;delete t.extraDeviceInfo}}o.emit("buoy:access",{protocol:1,ready:e});T()}}}var import_http=__toESM(require("http"));var import_socket=require("socket.io");var DEFAULT_BROKER_PORT=42831;var SOCKET_CONFIG={cors:{origin:"*",methods:["GET","POST"]},maxHttpBufferSize:1e8,transports:["websocket","polling"],pingTimeout:3e4,pingInterval:25e3};function createBroker(o={}){const n=o.port??DEFAULT_BROKER_PORT;const a=import_http.default.createServer(($,O)=>{O.writeHead(200,{"content-type":"text/plain"});O.end(`Buoy sync broker running on port ${n}`)});const u=new import_socket.Server(a,{...SOCKET_CONFIG,...o.socketOptions});const d=o.dashboardToken??createDashboardToken();const h=socketHandle({io:u,canUseDesktop:o.canUseDesktop,dashboardToken:d,originPolicy:o.originPolicy});a.on("error",$=>{o.onError?.($)});const f=()=>o.onListening?.(n);if(o.host)a.listen(n,o.host,f);else a.listen(n,f);const _=()=>new Promise($=>{u.close(()=>{a.close(()=>$())})});return{io:u,httpServer:a,port:n,dashboardToken:d,close:_,refreshAccess:h.refreshAccess}}var import_fs=__toESM(require("fs"));var import_os=__toESM(require("os"));var import_path=__toESM(require("path"));function buoyConfigDir(){if(process.platform==="win32"){const o=process.env.APPDATA||import_path.default.join(import_os.default.homedir(),"AppData","Roaming");return import_path.default.join(o,"Buoy")}return import_path.default.join(import_os.default.homedir(),".buoy")}var DEFAULT_PORT=42831;function brokerTokenPath(o=DEFAULT_PORT){const n=o===DEFAULT_PORT?"broker-token":`broker-token-${o}`;return import_path.default.join(buoyConfigDir(),n)}function writeBrokerToken(o,n=brokerTokenPath()){import_fs.default.mkdirSync(import_path.default.dirname(n),{recursive:true,mode:448});const a=`${n}.${process.pid}.tmp`;import_fs.default.writeFileSync(a,o,{mode:384});import_fs.default.renameSync(a,n);if(process.platform!=="win32"){try{import_fs.default.chmodSync(import_path.default.dirname(n),448)}catch{}}}function readBrokerToken(o=brokerTokenPath()){try{const n=import_fs.default.readFileSync(o,"utf8").trim();return n||null}catch{return null}}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var U=1;function M(r){if(r?.tier==="pro"||r?.tier==="free")return r.tier;if(r?.tier==="locked"||r?.tier==="anonymous")return"locked";return r?.isPro==="1"||r?.isPro===true?"pro":"locked"}var k=(r,a)=>JSON.stringify([r,a]);var R=r=>JSON.parse(r);var B=class{constructor(){this.byDashboard=new Map;this.streamCounts=new Map;this.captureCounts=new Map}counts(r){return r==="stream"?this.streamCounts:this.captureCounts}bump(r,a,l){const c=this.counts(r);const s=(c.get(a)??0)+l;if(s>0)c.set(a,s);else c.delete(a)}effective(r){if((this.streamCounts.get(r)??0)>0)return"stream";if((this.captureCounts.get(r)??0)>0)return"capture";return null}activeKeys(){return[...new Set([...this.streamCounts.keys(),...this.captureCounts.keys()])]}watch(r,a,l){let c=this.byDashboard.get(r);if(!c){c=new Map;this.byDashboard.set(r,c)}const s=c.get(a);if(s!==l){if(s)this.bump(s,a,-1);this.bump(l,a,1);c.set(a,l)}return this.effective(a)}unwatch(r,a){const l=this.byDashboard.get(r);const c=l?.get(a);if(!l||!c)return{changed:false,mode:this.effective(a)};const s=this.effective(a);l.delete(a);this.bump(c,a,-1);const g=this.effective(a);return{changed:g!==s,mode:g}}releaseDashboard(r){const a=this.byDashboard.get(r);if(!a)return[];this.byDashboard.delete(r);const l=[];for(const[c,s]of a){const g=this.effective(c);this.bump(s,c,-1);const I=this.effective(c);if(I!==g)l.push({key:c,mode:I})}return l}};var H=200;var z=24*60*60*1e3;function T(r){if(!r)return void 0;try{const a=JSON.parse(r);if(!a||typeof a!=="object"||Array.isArray(a))return void 0;for(const l of["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"])delete a[l];return JSON.stringify(a)}catch{return void 0}}function L({io:r,canUseDesktop:a=()=>false}){const l="[SERVER]";let c=[];const s=[];let g=[];const I=new Map;const w=new Map;const x=new Map;const O=new Map;const C=new Map;let S=new B;const A=[];function y(e,t,o){console.log(`${l} ${t}`);const d={ts:Date.now(),level:e,message:t,data:o};A.push(d);if(A.length>H){A.shift()}E("broker-log",d)}function q(e){const t=new Set;for(const[o,d]of I){if(d!==e)continue;const u=O.get(o);if(u)t.add(u)}return t.size}function N(){if(!a()){r.emit("all-devices-update",[]);return}for(const t of[...c,...s]){if(t.deviceName==="Dashboard"||!t.isConnected)continue;const o=w.get(t.id)??"locked";t.tier=o==="pro"?"pro":o==="free"?"free":"locked";t.isPro=o==="pro";if(o!=="free"&&o!=="pro"){delete t.envVariables;delete t.extraDeviceInfo}}const e=Date.now();for(const t of s){if(!t.deviceId)continue;const o=q(t.deviceId);const d=o>1;if(d&&!t.collision){y("warn",`${o} apps are registered as "${t.deviceId}" \u2014 actions sent to it land on an arbitrary one. Give each device its own externalSync.deviceId, or leave it unset for a per-install default.`,{deviceId:t.deviceId,instances:o})}else if(!d&&t.collision){y("info",`Device id collision cleared for "${t.deviceId}"`,{deviceId:t.deviceId})}t.collision=d}for(let t=s.length-1;t>=0;t--){const o=s[t];if(!o.isConnected&&o.lastSeenAt&&e-o.lastSeenAt>z){s.splice(t,1);if(o.deviceId){C.delete(o.deviceId)}}}r.emit("all-devices-update",s.filter(t=>t.deviceName!=="Dashboard").map(({licenseKey:t,...o})=>o))}function V(e,t){const o=c.find(h=>h.id===e);const d=I.get(e);I.delete(e);O.delete(e);w.delete(e);let u;if(d){for(const[h,b]of I){if(b===d)u=h}}if(d&&u){const h=c.findIndex(n=>n.deviceId===d);if(h!==-1&&c[h].id===e){c[h].id=u}const b=s.findIndex(n=>n.deviceId===d);if(b!==-1&&s[b].id===e){s[b].id=u}if(x.get(d)===e){x.set(d,u)}console.log(`${l} Client disconnected - ID: ${e}, Name: ${o?.deviceName||"Unknown"} - Device ${d} still connected via ${u}`)}else{c=c.filter(h=>h.id!==e);console.log(`${l} Client disconnected - ID: ${e}, Name: ${o?.deviceName||"Unknown"} - Removing user record`);if(d){const h=s.findIndex(b=>b.deviceId===d);if(h!==-1){s[h].isConnected=false;s[h].lastDisconnectReason=t;s[h].lastSeenAt=Date.now();x.delete(d)}}}const v=g.includes(e);g=g.filter(h=>h!==e);X(e);const $=Boolean(d&&u);y(v||$?"debug":"warn",$?`Socket closed for ${o?.deviceName||e} (${t??"unknown"}) \u2014 device still connected via another socket`:`Disconnected: ${o?.deviceName||e} (${t??"unknown"})`,{socketId:e,reason:t,wasDashboard:v,stillConnected:$});N();console.log(`${l} Updated users list: ${c.map(h=>h.deviceName).join(", ")}`)}function W({id:e,deviceName:t,deviceId:o,platform:d,extraDeviceInfo:u,envVariables:v,connectionNonce:$}){const h=t==="Dashboard";if(o){I.set(e,o);x.set(o,e);const i=c.findIndex(p=>p.deviceId===o);const f=s.findIndex(p=>p.deviceId===o);if(i!==-1){y("success",`Reconnected: ${t} (${d||"unknown platform"})`,{socketId:e,deviceId:o});c[i].id=e;if(d){c[i].platform=d}if(u){c[i].extraDeviceInfo=u}if(v){c[i].envVariables=v}c[i].isConnected=true;c[i].connectionNonce=$;if(f!==-1){s[f].id=e;s[f].isConnected=true;s[f].connectionNonce=$;s[f].lastSeenAt=Date.now();if(d){s[f].platform=d}if(u){s[f].extraDeviceInfo=u}if(v){s[f].envVariables=v}}else if(t!=="Dashboard"){s.push({id:e,deviceName:t,deviceId:o,platform:d,isConnected:true,extraDeviceInfo:u,envVariables:v,lastSeenAt:Date.now()})}N();console.log(`${l} Updated users list: ${c.map(p=>p.deviceName).join(", ")}`);return}}const b=i=>{const f=i.match(/(\d+)$/);return f?parseInt(f[1],10):1};let n=0;c.forEach(i=>{if(i.deviceName.startsWith(t)){const f=b(i.deviceName);if(f>=n){n=f}}});if(n>0&&!h){const i=`${t} #${n+1}`;y("warn",`Duplicate device name "${t}" \u2014 renamed to "${i}"`,{socketId:e,deviceId:o});t=i}if(e){const i={id:e,deviceName:t,deviceId:o,platform:d,isConnected:true,extraDeviceInfo:u,envVariables:v,connectionNonce:$,lastSeenAt:Date.now()};c.push(i);if(t!=="Dashboard"&&o){const f=s.findIndex(p=>p.deviceId===o);if(f!==-1){s[f]={...i}}else{s.push({...i})}}y(h?"debug":"info",`Connected: ${t}${d?` (${d})`:""}`,{socketId:e,deviceId:o});if(h&&e){g.push(e);console.log(`${l} Registered dashboard client with ID: ${e}`)}N();console.log(`${l} Updated device list: ${c.map(f=>f.deviceName).join(", ")}`)}}function j(e){return s.find(t=>t.deviceId===e)}function G(e,t){console.log(`${l} Message from ${t}: ${e}`);if(!a())return;r.emit("message",`Device ${t} sent message: ${e}`)}function E(e,t){if(!a())return;g=g.filter(o=>r.sockets.sockets.has(o));if(g.length===0){return}for(const o of g){r.sockets.sockets.get(o)?.emit(e,t)}}function _(e,t,o){if(!a())return;const d=v=>{const $=w.get(v.id);return $==="free"||$==="pro"};const u=j(e)?.deviceName;if(e==="No devices available"){console.log(`${l} Skipping ${o} - No devices available`);return}if(e==="All"){console.log(`${l} Broadcasting ${o} to all devices`);const v=c.filter($=>$.deviceName!=="Dashboard");console.log(`${l} Sending to ${v.length} devices: ${v.map($=>$.deviceName).join(", ")}`);v.filter(d).forEach(t)}else{const v=c.find($=>$.deviceId===e);if(v&&d(v)){console.log(`${l} Sending to target device ${v.deviceName} (${v.deviceId}) targetDeviceId: ${e}`);t(v)}else{console.log(`${l} Target device not found - DeviceId: ${e}, DeviceName: ${u}`)}}}function K(e,t){const[o,d]=R(e);const u=t===null?{toolId:d,targetDeviceId:o,watching:false,consolidated:true}:{toolId:d,targetDeviceId:o,watching:true,stream:t==="stream",consolidated:true};_(o,v=>r.to(v.id).emit("devtool-watch",u),`devtool ${t===null?"unwatch":`watch(${t})`} ${d}`)}function J(e,t){const o=k(t.targetDeviceId,t.toolId);if(t.watching){const v=t.stream===false?"capture":"stream";const $=S.watch(e,o,v);console.log(`${l} Dashboard ${e} watching "${t.toolId}" on ${t.targetDeviceId} (${v}, effective ${$})`);K(o,$);return}const{changed:d,mode:u}=S.unwatch(e,o);if(!d)return;console.log(u===null?`${l} Last dashboard stopped watching "${t.toolId}" on ${t.targetDeviceId}`:`${l} "${t.toolId}" on ${t.targetDeviceId} downgraded to ${u}`);K(o,u)}function X(e){for(const{key:t,mode:o}of S.releaseDashboard(e)){K(t,o)}}r.on("connection",e=>{const t=()=>e.emit("buoy:access",{protocol:1,ready:a()});t();e.on("buoy:access:request",t);e.use((n,i)=>{if(n[0]==="buoy:access:request"){i();return}if(!a()){t();return}const f=n[0]==="device-license";if(f){const D=n[1]??{};w.set(e.id,M(D))}const p=w.get(e.id)==="free"||w.get(e.id)==="pro";if(f||g.includes(e.id)||p)i();else t()});const{deviceName:o,deviceId:d,platform:u,extraDeviceInfo:v,envVariables:$,connectionNonce:h,instanceId:b}=e.handshake.query;if(typeof b==="string"&&b){O.set(e.id,b)}console.log(`${l} New connection - ID: ${e.id}, Name: ${o||"Unknown Device"}${d?`, DeviceId: ${d}`:""}${u?`, Platform: ${u}`:""}`);W({id:e.id,deviceName:o||"Unknown Device Name",deviceId:d,platform:u,extraDeviceInfo:void 0,envVariables:void 0,connectionNonce:h});if(a()&&o==="Dashboard"){for(const n of C.values()){e.emit("devtool-capabilities",n)}for(const n of A){e.emit("broker-log",n)}}e.on("device-license",n=>{const i=M(n??{});const f=i==="pro";w.set(e.id,i);const p=c.find(m=>m.id===e.id);if(p){p.isPro=f;p.tier=i;delete p.licenseKey}const D=p?.deviceId||I.get(e.id);if(i!=="locked"){for(const m of[...c,...s].filter(P=>P.id===e.id)){m.extraDeviceInfo=T(v);m.envVariables=T($)}}if(D){if(i==="locked")C.delete(D);const m=s.find(P=>P.deviceId===D);if(m){m.isPro=f;m.tier=i;delete m.licenseKey}}y("debug",`device-license from ${p?.deviceName||e.id}: tier=${i}`,{socketId:e.id});N()});e.on("disconnect",n=>V(e.id,n));e.on("message",n=>{G(n,o||"Unknown")});e.on("devtool-capabilities",n=>{if(!n||!Array.isArray(n.tools)||n.persistentDeviceId!==I.get(e.id))return;console.log(`${l} Capabilities from ${n.persistentDeviceId}: ${n.tools.map(i=>`${i.toolId}@v${i.version}`).join(", ")}`);if(n.protocolVersion!==U){y("warn",`Protocol version mismatch from ${n.persistentDeviceId}: device speaks v${n.protocolVersion}, broker speaks v${U} \u2014 update @buoy-gg packages or Buoy Desktop`,{deviceId:n.persistentDeviceId})}C.set(n.persistentDeviceId,n);E("devtool-capabilities",n);for(const i of S.activeKeys()){const[f,p]=R(i);if(f!=="All"&&f!==n.persistentDeviceId){continue}const D=S.effective(i);if(!D)continue;e.emit("devtool-watch",{toolId:p,targetDeviceId:f,watching:true,stream:D==="stream",consolidated:true})}});e.on("devtool-sync",n=>{if(!n||n.persistentDeviceId!==I.get(e.id))return;E("devtool-sync",n)});e.on("devtool-action-result",n=>{if(!n||n.persistentDeviceId!==I.get(e.id))return;console.log(`${l} Action result from ${n.persistentDeviceId}: ${n.toolId}.${n.action} -> ${n.ok?"ok":`error: ${n.error}`}`);E("devtool-action-result",n)});e.on("devtool-watch",n=>{J(e.id,n)});e.on("request-devtool-state",n=>{_(n.targetDeviceId,i=>r.to(i.id).emit("request-devtool-state",n),`devtool state request ${n.toolId}`)});e.on("forget-device",n=>{const i=n?.deviceId;if(!g.includes(e.id))return;if(!i||x.has(i))return;const f=s.find(D=>D.deviceId===i);c=c.filter(D=>D.deviceId!==i);const p=s.findIndex(D=>D.deviceId===i);if(p!==-1){s.splice(p,1)}C.delete(i);for(const[D,m]of I){if(m===i){I.delete(D)}}y("info",`Forgot device: ${f?.deviceName??i}`,{deviceId:i});N()});e.on("devtool-action",n=>{console.log(`${l} Devtool action from dashboard: ${n.toolId}.${n.action} -> ${n.targetDeviceId}`);_(n.targetDeviceId,i=>r.to(i.id).emit("devtool-action",n),`devtool action ${n.toolId}.${n.action}`)})});return{refreshAccess(){const e=a();if(!e){w.clear();C.clear();A.length=0;S=new B;for(const t of[...c,...s]){delete t.licenseKey;delete t.envVariables;delete t.extraDeviceInfo}}r.emit("buoy:access",{protocol:1,ready:e});N()}}}import Q from"http";import{Server as Z}from"socket.io";var Y=42831;var F={cors:{origin:"*",methods:["GET","POST"]},maxHttpBufferSize:1e8,transports:["websocket","polling"],pingTimeout:3e4,pingInterval:25e3};function ee(r={}){const a=r.port??Y;const l=Q.createServer((I,w)=>{w.writeHead(200,{"content-type":"text/plain"});w.end(`Buoy sync broker running on port ${a}`)});const c=new Z(l,{...F,...r.socketOptions});const s=L({io:c,canUseDesktop:r.canUseDesktop});l.on("error",I=>{r.onError?.(I)});l.listen(a,()=>{r.onListening?.(a)});const g=()=>new Promise(I=>{c.close(()=>{l.close(()=>I())})});return{io:c,httpServer:l,port:a,close:g,refreshAccess:s.refreshAccess}}export{Y as DEFAULT_BROKER_PORT,F as SOCKET_CONFIG,U as SYNC_PROTOCOL_VERSION,ee as createBroker,L as socketHandle};
|
|
1
|
+
var Y=1;function G(o){if(o?.tier==="pro"||o?.tier==="free")return o.tier;if(o?.tier==="locked"||o?.tier==="anonymous")return"locked";return o?.isPro==="1"||o?.isPro===true?"pro":"locked"}var he=(o,i)=>JSON.stringify([o,i]);var V=o=>JSON.parse(o);var H=class{constructor(){this.byDashboard=new Map;this.streamCounts=new Map;this.captureCounts=new Map}counts(o){return o==="stream"?this.streamCounts:this.captureCounts}bump(o,i,d){const v=this.counts(o);const f=(v.get(i)??0)+d;if(f>0)v.set(i,f);else v.delete(i)}effective(o){if((this.streamCounts.get(o)??0)>0)return"stream";if((this.captureCounts.get(o)??0)>0)return"capture";return null}activeKeys(){return[...new Set([...this.streamCounts.keys(),...this.captureCounts.keys()])]}watch(o,i,d){let v=this.byDashboard.get(o);if(!v){v=new Map;this.byDashboard.set(o,v)}const f=v.get(i);if(f!==d){if(f)this.bump(f,i,-1);this.bump(d,i,1);v.set(i,d)}return this.effective(i)}unwatch(o,i){const d=this.byDashboard.get(o);const v=d?.get(i);if(!d||!v)return{changed:false,mode:this.effective(i)};const f=this.effective(i);d.delete(i);this.bump(v,i,-1);const h=this.effective(i);return{changed:h!==f,mode:h}}releaseDashboard(o){const i=this.byDashboard.get(o);if(!i)return[];this.byDashboard.delete(o);const d=[];for(const[v,f]of i){const h=this.effective(v);this.bump(f,v,-1);const l=this.effective(v);if(l!==h)d.push({key:v,mode:l})}return d}};import{randomBytes as pe,timingSafeEqual as $e}from"crypto";var Ie=1;var q="buoy:dashboard-token-required: this broker only accepts dashboards that present its token. Update Buoy Desktop and @buoy-gg/mcp.";var X="buoy:origin-not-allowed: this site isn't allowed to connect to Buoy. Approve it in Buoy Desktop.";function J(){return pe(32).toString("hex")}function W(o,i){if(typeof i!=="string"||!o||!i)return false;const d=Buffer.from(o);const v=Buffer.from(i);return d.length===v.length&&$e(d,v)}function z(o){const i=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(o);if(!i)return false;const[d,v]=[Number(i[1]),Number(i[2])];return d===127||d===10||d===172&&v>=16&&v<=31||d===192&&v===168||d===169&&v===254}function me(o){if(o==="localhost"||o.endsWith(".localhost"))return true;if(o.endsWith(".local")||o.endsWith(".test"))return true;if(z(o))return true;if(o==="::1")return true;const i=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(o);if(i)return z(i[1]);return/^f[cd][0-9a-f]{0,2}:/.test(o)||/^fe[89ab][0-9a-f]?:/.test(o)}function Q(o){let i;try{i=new URL(o)}catch{return false}if(i.protocol==="file:")return true;const d=i.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(i.protocol==="tauri:"||i.protocol==="capacitor:"||i.protocol==="ionic:"){return d==="localhost"}if(i.protocol!=="http:"&&i.protocol!=="https:")return false;return me(d)}var be=12e4;var ge=200;var De=24*60*60*1e3;function Z(o){if(!o)return void 0;try{const i=JSON.parse(o);if(!i||typeof i!=="object"||Array.isArray(i))return void 0;for(const d of["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","NEXT_PUBLIC_BUOY_KEY","REACT_APP_BUOY_KEY","licenseKey"])delete i[d];return JSON.stringify(i)}catch{return void 0}}function ee({io:o,canUseDesktop:i=()=>false,dashboardToken:d,originPolicy:v}){const f="[SERVER]";let h=[];const l=[];let w=[];const m=new Map;const S=new Map;const T=new Map;const P=new Map;const O=new Map;let E=new H;const B=[];function N(e,t,r){console.log(`${f} ${t}`);const s={ts:Date.now(),level:e,message:t,data:r};B.push(s);if(B.length>ge){B.shift()}R("broker-log",s)}function ce(e){const t=new Set;for(const[r,s]of m){if(s!==e)continue;const u=P.get(r);if(u)t.add(u)}return t.size}function _(){if(!i()){U("all-devices-update",[]);return}for(const t of[...h,...l]){if(t.deviceName==="Dashboard"||!t.isConnected)continue;const r=S.get(t.id)??"locked";t.tier=r==="pro"?"pro":r==="free"?"free":"locked";t.isPro=r==="pro";if(r!=="free"&&r!=="pro"){delete t.envVariables;delete t.extraDeviceInfo}}const e=Date.now();for(const t of l){if(!t.deviceId)continue;const r=ce(t.deviceId);const s=r>1;if(s&&!t.collision){N("warn",`${r} apps are registered as "${t.deviceId}" \u2014 actions sent to it land on an arbitrary one. Give each device its own externalSync.deviceId, or leave it unset for a per-install default.`,{deviceId:t.deviceId,instances:r})}else if(!s&&t.collision){N("info",`Device id collision cleared for "${t.deviceId}"`,{deviceId:t.deviceId})}t.collision=s}for(let t=l.length-1;t>=0;t--){const r=l[t];if(!r.isConnected&&r.lastSeenAt&&e-r.lastSeenAt>De){l.splice(t,1);if(r.deviceId){O.delete(r.deviceId)}}}U("all-devices-update",l.filter(t=>t.deviceName!=="Dashboard").map(({licenseKey:t,...r})=>r))}function se(e,t){const r=h.find(p=>p.id===e);const s=m.get(e);m.delete(e);P.delete(e);S.delete(e);let u;if(s){for(const[p,g]of m){if(g===s)u=p}}if(s&&u){const p=h.findIndex(y=>y.deviceId===s);if(p!==-1&&h[p].id===e){h[p].id=u}const g=l.findIndex(y=>y.deviceId===s);if(g!==-1&&l[g].id===e){l[g].id=u}if(T.get(s)===e){T.set(s,u)}console.log(`${f} Client disconnected - ID: ${e}, Name: ${r?.deviceName||"Unknown"} - Device ${s} still connected via ${u}`)}else{h=h.filter(p=>p.id!==e);console.log(`${f} Client disconnected - ID: ${e}, Name: ${r?.deviceName||"Unknown"} - Removing user record`);if(s){const p=l.findIndex(g=>g.deviceId===s);if(p!==-1){l[p].isConnected=false;l[p].lastDisconnectReason=t;l[p].lastSeenAt=Date.now();T.delete(s)}}}const a=w.includes(e);w=w.filter(p=>p!==e);ve(e);const $=Boolean(s&&u);N(a||$?"debug":"warn",$?`Socket closed for ${r?.deviceName||e} (${t??"unknown"}) \u2014 device still connected via another socket`:`Disconnected: ${r?.deviceName||e} (${t??"unknown"})`,{socketId:e,reason:t,wasDashboard:a,stillConnected:$});_();console.log(`${f} Updated users list: ${h.map(p=>p.deviceName).join(", ")}`)}function de({id:e,deviceName:t,deviceId:r,platform:s,extraDeviceInfo:u,envVariables:a,connectionNonce:$},p){if(r){m.set(e,r);T.set(r,e);const n=h.findIndex(I=>I.deviceId===r);const c=l.findIndex(I=>I.deviceId===r);if(n!==-1){N("success",`Reconnected: ${t} (${s||"unknown platform"})`,{socketId:e,deviceId:r});h[n].id=e;if(s){h[n].platform=s}if(u){h[n].extraDeviceInfo=u}if(a){h[n].envVariables=a}h[n].isConnected=true;h[n].connectionNonce=$;if(c!==-1){l[c].id=e;l[c].isConnected=true;l[c].connectionNonce=$;l[c].lastSeenAt=Date.now();if(s){l[c].platform=s}if(u){l[c].extraDeviceInfo=u}if(a){l[c].envVariables=a}}else if(t!=="Dashboard"){l.push({id:e,deviceName:t,deviceId:r,platform:s,isConnected:true,extraDeviceInfo:u,envVariables:a,lastSeenAt:Date.now()})}_();console.log(`${f} Updated users list: ${h.map(I=>I.deviceName).join(", ")}`);return}}const g=n=>{const c=n.match(/(\d+)$/);return c?parseInt(c[1],10):1};let y=0;h.forEach(n=>{if(n.deviceName.startsWith(t)){const c=g(n.deviceName);if(c>=y){y=c}}});if(y>0&&!p){const n=`${t} #${y+1}`;N("warn",`Duplicate device name "${t}" \u2014 renamed to "${n}"`,{socketId:e,deviceId:r});t=n}if(e){const n={id:e,deviceName:t,deviceId:r,platform:s,isConnected:true,extraDeviceInfo:u,envVariables:a,connectionNonce:$,lastSeenAt:Date.now()};h.push(n);if(t!=="Dashboard"&&r){const c=l.findIndex(I=>I.deviceId===r);if(c!==-1){l[c]={...n}}else{l.push({...n})}}N(p?"debug":"info",`Connected: ${t}${s?` (${s})`:""}`,{socketId:e,deviceId:r});if(p&&e){w.push(e);console.log(`${f} Registered dashboard client with ID: ${e}`)}_();console.log(`${f} Updated device list: ${h.map(c=>c.deviceName).join(", ")}`)}}function ae(e){return l.find(t=>t.deviceId===e)}function le(e,t){console.log(`${f} Message from ${t}: ${e}`)}function R(e,t){if(!i())return;U(e,t)}function U(e,t){w=w.filter(r=>o.sockets.sockets.has(r));for(const r of w){o.sockets.sockets.get(r)?.emit(e,t)}}function x(e){return w.includes(e)}const K=new Map;function fe(e,t){let r=K.get(e);if(!r){let s;const u=new Promise(a=>{s=setTimeout(()=>a(false),be)});r=Promise.race([t(e).catch(()=>false),u]).finally(()=>{clearTimeout(s);K.delete(e)});K.set(e,r)}return r}o.use((e,t)=>{const{auth:r,query:s,headers:u}=e.handshake;if(W(d,r?.dashboardToken)){t();return}if(s?.deviceName==="Dashboard"){N("warn","Refused a dashboard connection without the broker token (old Buoy Desktop or @buoy-gg/mcp?)",{address:e.handshake.address});t(new Error(q));return}const a=typeof u?.origin==="string"?u.origin:void 0;if(!a||Q(a)){t();return}const $=v?.remembered(a);if($===true){t();return}const p=()=>{N("warn",`Refused a connection from ${a}. Approve the site in Buoy Desktop to let it connect.`,{origin:a});t(new Error(X))};if($===false||!v?.ask){p();return}void fe(a,v.ask).then(g=>g?t():p())});function L(e,t,r){if(!i())return;const s=a=>{const $=S.get(a.id);return $==="free"||$==="pro"};const u=ae(e)?.deviceName;if(e==="No devices available"){console.log(`${f} Skipping ${r} - No devices available`);return}if(e==="All"){console.log(`${f} Broadcasting ${r} to all devices`);const a=h.filter($=>$.deviceName!=="Dashboard");console.log(`${f} Sending to ${a.length} devices: ${a.map($=>$.deviceName).join(", ")}`);a.filter(s).forEach(t)}else{const a=h.find($=>$.deviceId===e);if(a&&s(a)){console.log(`${f} Sending to target device ${a.deviceName} (${a.deviceId}) targetDeviceId: ${e}`);t(a)}else{console.log(`${f} Target device not found - DeviceId: ${e}, DeviceName: ${u}`)}}}function M(e,t){const[r,s]=V(e);const u=t===null?{toolId:s,targetDeviceId:r,watching:false,consolidated:true}:{toolId:s,targetDeviceId:r,watching:true,stream:t==="stream",consolidated:true};L(r,a=>o.to(a.id).emit("devtool-watch",u),`devtool ${t===null?"unwatch":`watch(${t})`} ${s}`)}function ue(e,t){const r=he(t.targetDeviceId,t.toolId);if(t.watching){const a=t.stream===false?"capture":"stream";const $=E.watch(e,r,a);console.log(`${f} Dashboard ${e} watching "${t.toolId}" on ${t.targetDeviceId} (${a}, effective ${$})`);M(r,$);return}const{changed:s,mode:u}=E.unwatch(e,r);if(!s)return;console.log(u===null?`${f} Last dashboard stopped watching "${t.toolId}" on ${t.targetDeviceId}`:`${f} "${t.toolId}" on ${t.targetDeviceId} downgraded to ${u}`);M(r,u)}function ve(e){for(const{key:t,mode:r}of E.releaseDashboard(e)){M(t,r)}}o.on("connection",e=>{const t=W(d,e.handshake.auth?.dashboardToken);if(!t&&e.handshake.query?.deviceName==="Dashboard"){e.emit("buoy:auth-error",{reason:q});e.disconnect(true);return}const r=()=>e.emit("buoy:access",{protocol:1,ready:i()});r();e.on("buoy:access:request",r);e.use((n,c)=>{if(n[0]==="buoy:access:request"){c();return}if(!i()){r();return}const I=n[0]==="device-license";if(I){const b=n[1]??{};S.set(e.id,G(b))}const D=S.get(e.id)==="free"||S.get(e.id)==="pro";if(I||w.includes(e.id)||D)c();else r()});const{deviceName:s,deviceId:u,platform:a,extraDeviceInfo:$,envVariables:p,connectionNonce:g,instanceId:y}=e.handshake.query;if(typeof y==="string"&&y){P.set(e.id,y)}console.log(`${f} New connection - ID: ${e.id}, Name: ${s||"Unknown Device"}${u?`, DeviceId: ${u}`:""}${a?`, Platform: ${a}`:""}`);de({id:e.id,deviceName:s||"Unknown Device Name",deviceId:u,platform:a,extraDeviceInfo:void 0,envVariables:void 0,connectionNonce:g},t);if(i()&&t){for(const n of O.values()){e.emit("devtool-capabilities",n)}for(const n of B){e.emit("broker-log",n)}}e.on("device-license",n=>{const c=G(n??{});const I=c==="pro";S.set(e.id,c);const D=h.find(A=>A.id===e.id);if(D){D.isPro=I;D.tier=c;delete D.licenseKey}const b=D?.deviceId||m.get(e.id);if(c!=="locked"){for(const A of[...h,...l].filter(F=>F.id===e.id)){A.extraDeviceInfo=Z($);A.envVariables=Z(p)}}if(b){if(c==="locked")O.delete(b);const A=l.find(F=>F.deviceId===b);if(A){A.isPro=I;A.tier=c;delete A.licenseKey}}N("debug",`device-license from ${D?.deviceName||e.id}: tier=${c}`,{socketId:e.id});_()});e.on("disconnect",n=>se(e.id,n));e.on("message",n=>{le(n,s||"Unknown")});e.on("devtool-capabilities",n=>{if(!n||!Array.isArray(n.tools)||n.persistentDeviceId!==m.get(e.id))return;console.log(`${f} Capabilities from ${n.persistentDeviceId}: ${n.tools.map(c=>`${c.toolId}@v${c.version}`).join(", ")}`);if(n.protocolVersion!==Y){N("warn",`Protocol version mismatch from ${n.persistentDeviceId}: device speaks v${n.protocolVersion}, broker speaks v${Y} \u2014 update @buoy-gg packages or Buoy Desktop`,{deviceId:n.persistentDeviceId})}O.set(n.persistentDeviceId,n);R("devtool-capabilities",n);for(const c of E.activeKeys()){const[I,D]=V(c);if(I!=="All"&&I!==n.persistentDeviceId){continue}const b=E.effective(c);if(!b)continue;e.emit("devtool-watch",{toolId:D,targetDeviceId:I,watching:true,stream:b==="stream",consolidated:true})}});e.on("devtool-sync",n=>{if(!n||n.persistentDeviceId!==m.get(e.id))return;R("devtool-sync",n)});e.on("devtool-action-result",n=>{if(!n||n.persistentDeviceId!==m.get(e.id))return;console.log(`${f} Action result from ${n.persistentDeviceId}: ${n.toolId}.${n.action} -> ${n.ok?"ok":`error: ${n.error}`}`);R("devtool-action-result",n)});e.on("devtool-watch",n=>{if(!x(e.id))return;ue(e.id,n)});e.on("request-devtool-state",n=>{if(!x(e.id))return;L(n.targetDeviceId,c=>o.to(c.id).emit("request-devtool-state",n),`devtool state request ${n.toolId}`)});e.on("forget-device",n=>{const c=n?.deviceId;if(!w.includes(e.id))return;if(!c||T.has(c))return;const I=l.find(b=>b.deviceId===c);h=h.filter(b=>b.deviceId!==c);const D=l.findIndex(b=>b.deviceId===c);if(D!==-1){l.splice(D,1)}O.delete(c);for(const[b,A]of m){if(A===c){m.delete(b)}}N("info",`Forgot device: ${I?.deviceName??c}`,{deviceId:c});_()});e.on("devtool-action",n=>{if(!x(e.id))return;console.log(`${f} Devtool action from dashboard: ${n.toolId}.${n.action} -> ${n.targetDeviceId}`);L(n.targetDeviceId,c=>o.to(c.id).emit("devtool-action",n),`devtool action ${n.toolId}.${n.action}`)})});return{refreshAccess(){const e=i();if(!e){S.clear();O.clear();B.length=0;E=new H;for(const t of[...h,...l]){delete t.licenseKey;delete t.envVariables;delete t.extraDeviceInfo}}o.emit("buoy:access",{protocol:1,ready:e});_()}}}import we from"http";import{Server as ye}from"socket.io";var te=42831;var ne={cors:{origin:"*",methods:["GET","POST"]},maxHttpBufferSize:1e8,transports:["websocket","polling"],pingTimeout:3e4,pingInterval:25e3};function Se(o={}){const i=o.port??te;const d=we.createServer((m,S)=>{S.writeHead(200,{"content-type":"text/plain"});S.end(`Buoy sync broker running on port ${i}`)});const v=new ye(d,{...ne,...o.socketOptions});const f=o.dashboardToken??J();const h=ee({io:v,canUseDesktop:o.canUseDesktop,dashboardToken:f,originPolicy:o.originPolicy});d.on("error",m=>{o.onError?.(m)});const l=()=>o.onListening?.(i);if(o.host)d.listen(i,o.host,l);else d.listen(i,l);const w=()=>new Promise(m=>{v.close(()=>{d.close(()=>m())})});return{io:v,httpServer:d,port:i,dashboardToken:f,close:w,refreshAccess:h.refreshAccess}}import k from"fs";import oe from"os";import C from"path";function ie(){if(process.platform==="win32"){const o=process.env.APPDATA||C.join(oe.homedir(),"AppData","Roaming");return C.join(o,"Buoy")}return C.join(oe.homedir(),".buoy")}var re=42831;function j(o=re){const i=o===re?"broker-token":`broker-token-${o}`;return C.join(ie(),i)}function Ne(o,i=j()){k.mkdirSync(C.dirname(i),{recursive:true,mode:448});const d=`${i}.${process.pid}.tmp`;k.writeFileSync(d,o,{mode:384});k.renameSync(d,i);if(process.platform!=="win32"){try{k.chmodSync(C.dirname(i),448)}catch{}}}function Ae(o=j()){try{const i=k.readFileSync(o,"utf8").trim();return i||null}catch{return null}}export{Ie as DASHBOARD_AUTH_VERSION,q as DASHBOARD_TOKEN_REQUIRED,te as DEFAULT_BROKER_PORT,X as ORIGIN_NOT_ALLOWED,ne as SOCKET_CONFIG,Y as SYNC_PROTOCOL_VERSION,j as brokerTokenPath,ie as buoyConfigDir,Se as createBroker,J as createDashboardToken,Q as isLocalOrigin,Ae as readBrokerToken,ee as socketHandle,W as tokenMatches,Ne as writeBrokerToken};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/sync-broker",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.44",
|
|
4
4
|
"description": "Headless Socket.IO broker for the Buoy devtool sync protocol (v1) — routes messages between devices and dashboard/MCP clients",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|