@buoy-gg/sync-broker 7.0.41 → 7.0.43
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.js +1 -764
- package/dist/index.mjs +1 -723
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,764 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
DEFAULT_BROKER_PORT: () => DEFAULT_BROKER_PORT,
|
|
34
|
-
SOCKET_CONFIG: () => SOCKET_CONFIG,
|
|
35
|
-
SYNC_PROTOCOL_VERSION: () => SYNC_PROTOCOL_VERSION,
|
|
36
|
-
createBroker: () => createBroker,
|
|
37
|
-
socketHandle: () => socketHandle
|
|
38
|
-
});
|
|
39
|
-
module.exports = __toCommonJS(index_exports);
|
|
40
|
-
|
|
41
|
-
// src/protocol.ts
|
|
42
|
-
var SYNC_PROTOCOL_VERSION = 1;
|
|
43
|
-
function tierFromLicensePayload(payload) {
|
|
44
|
-
if (payload?.tier === "pro" || payload?.tier === "free") return payload.tier;
|
|
45
|
-
if (payload?.tier === "locked" || payload?.tier === "anonymous") return "locked";
|
|
46
|
-
return payload?.isPro === "1" || payload?.isPro === true ? "pro" : "locked";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// src/watchRegistry.ts
|
|
50
|
-
var toWatchKey = (targetDeviceId, toolId) => JSON.stringify([targetDeviceId, toolId]);
|
|
51
|
-
var fromWatchKey = (key) => JSON.parse(key);
|
|
52
|
-
var WatchRegistry = class {
|
|
53
|
-
constructor() {
|
|
54
|
-
/** Per dashboard: the key it watches → the mode it asked for. */
|
|
55
|
-
this.byDashboard = /* @__PURE__ */ new Map();
|
|
56
|
-
this.streamCounts = /* @__PURE__ */ new Map();
|
|
57
|
-
this.captureCounts = /* @__PURE__ */ new Map();
|
|
58
|
-
}
|
|
59
|
-
counts(mode) {
|
|
60
|
-
return mode === "stream" ? this.streamCounts : this.captureCounts;
|
|
61
|
-
}
|
|
62
|
-
bump(mode, key, delta) {
|
|
63
|
-
const counts = this.counts(mode);
|
|
64
|
-
const next = (counts.get(key) ?? 0) + delta;
|
|
65
|
-
if (next > 0) counts.set(key, next);
|
|
66
|
-
else counts.delete(key);
|
|
67
|
-
}
|
|
68
|
-
/** The strongest mode any dashboard currently wants; null for nobody. */
|
|
69
|
-
effective(key) {
|
|
70
|
-
if ((this.streamCounts.get(key) ?? 0) > 0) return "stream";
|
|
71
|
-
if ((this.captureCounts.get(key) ?? 0) > 0) return "capture";
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
/** Every key with at least one watcher (for replay to a reconnecting device). */
|
|
75
|
-
activeKeys() {
|
|
76
|
-
return [
|
|
77
|
-
.../* @__PURE__ */ new Set([...this.streamCounts.keys(), ...this.captureCounts.keys()])
|
|
78
|
-
];
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Record a watch. Returns the mode to send the device — always non-null,
|
|
82
|
-
* because the device answers a watch with a snapshot and that is how a
|
|
83
|
-
* dashboard joining a quiet tool gets its first paint. Re-watching at the
|
|
84
|
-
* same mode is therefore NOT a no-op at the socket layer.
|
|
85
|
-
*/
|
|
86
|
-
watch(dashboardId, key, mode) {
|
|
87
|
-
let held = this.byDashboard.get(dashboardId);
|
|
88
|
-
if (!held) {
|
|
89
|
-
held = /* @__PURE__ */ new Map();
|
|
90
|
-
this.byDashboard.set(dashboardId, held);
|
|
91
|
-
}
|
|
92
|
-
const previous = held.get(key);
|
|
93
|
-
if (previous !== mode) {
|
|
94
|
-
if (previous) this.bump(previous, key, -1);
|
|
95
|
-
this.bump(mode, key, 1);
|
|
96
|
-
held.set(key, mode);
|
|
97
|
-
}
|
|
98
|
-
return this.effective(key);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Drop one dashboard's claim. Returns the new effective mode ONLY when it
|
|
102
|
-
* changed — the device is left alone while someone else still wants the
|
|
103
|
-
* same thing.
|
|
104
|
-
*/
|
|
105
|
-
unwatch(dashboardId, key) {
|
|
106
|
-
const held = this.byDashboard.get(dashboardId);
|
|
107
|
-
const mode = held?.get(key);
|
|
108
|
-
if (!held || !mode) return { changed: false, mode: this.effective(key) };
|
|
109
|
-
const before = this.effective(key);
|
|
110
|
-
held.delete(key);
|
|
111
|
-
this.bump(mode, key, -1);
|
|
112
|
-
const after = this.effective(key);
|
|
113
|
-
return { changed: after !== before, mode: after };
|
|
114
|
-
}
|
|
115
|
-
/** Release everything a disconnecting dashboard held. */
|
|
116
|
-
releaseDashboard(dashboardId) {
|
|
117
|
-
const held = this.byDashboard.get(dashboardId);
|
|
118
|
-
if (!held) return [];
|
|
119
|
-
this.byDashboard.delete(dashboardId);
|
|
120
|
-
const changes = [];
|
|
121
|
-
for (const [key, mode] of held) {
|
|
122
|
-
const before = this.effective(key);
|
|
123
|
-
this.bump(mode, key, -1);
|
|
124
|
-
const after = this.effective(key);
|
|
125
|
-
if (after !== before) changes.push({ key, mode: after });
|
|
126
|
-
}
|
|
127
|
-
return changes;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
// src/socketHandle.ts
|
|
132
|
-
var BROKER_LOG_BUFFER_MAX = 200;
|
|
133
|
-
var MAX_OFFLINE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
134
|
-
function redactAccountKeys(raw) {
|
|
135
|
-
if (!raw) return void 0;
|
|
136
|
-
try {
|
|
137
|
-
const value = JSON.parse(raw);
|
|
138
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
139
|
-
for (const key of ["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY", "licenseKey"]) delete value[key];
|
|
140
|
-
return JSON.stringify(value);
|
|
141
|
-
} catch {
|
|
142
|
-
return void 0;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function socketHandle({ io, canUseDesktop = () => false }) {
|
|
146
|
-
const LOG_PREFIX = "[SERVER]";
|
|
147
|
-
let users = [];
|
|
148
|
-
const allDevices = [];
|
|
149
|
-
let dashboardClients = [];
|
|
150
|
-
const deviceIdMap = /* @__PURE__ */ new Map();
|
|
151
|
-
const socketTiers = /* @__PURE__ */ new Map();
|
|
152
|
-
const activeConnectionsMap = /* @__PURE__ */ new Map();
|
|
153
|
-
const instanceIdMap = /* @__PURE__ */ new Map();
|
|
154
|
-
const deviceCapabilities = /* @__PURE__ */ new Map();
|
|
155
|
-
let watches = new WatchRegistry();
|
|
156
|
-
const brokerLogBuffer = [];
|
|
157
|
-
function brokerLog(level, message, data) {
|
|
158
|
-
console.log(`${LOG_PREFIX} ${message}`);
|
|
159
|
-
const entry = { ts: Date.now(), level, message, data };
|
|
160
|
-
brokerLogBuffer.push(entry);
|
|
161
|
-
if (brokerLogBuffer.length > BROKER_LOG_BUFFER_MAX) {
|
|
162
|
-
brokerLogBuffer.shift();
|
|
163
|
-
}
|
|
164
|
-
forwardToDashboards("broker-log", entry);
|
|
165
|
-
}
|
|
166
|
-
function liveInstances(deviceId) {
|
|
167
|
-
const instances = /* @__PURE__ */ new Set();
|
|
168
|
-
for (const [socketId, mappedDeviceId] of deviceIdMap) {
|
|
169
|
-
if (mappedDeviceId !== deviceId) continue;
|
|
170
|
-
const instance = instanceIdMap.get(socketId);
|
|
171
|
-
if (instance) instances.add(instance);
|
|
172
|
-
}
|
|
173
|
-
return instances.size;
|
|
174
|
-
}
|
|
175
|
-
function emitAllDevices() {
|
|
176
|
-
if (!canUseDesktop()) {
|
|
177
|
-
io.emit("all-devices-update", []);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
for (const device of [...users, ...allDevices]) {
|
|
181
|
-
if (device.deviceName === "Dashboard" || !device.isConnected) continue;
|
|
182
|
-
const tier = socketTiers.get(device.id) ?? "locked";
|
|
183
|
-
device.tier = tier === "pro" ? "pro" : tier === "free" ? "free" : "locked";
|
|
184
|
-
device.isPro = tier === "pro";
|
|
185
|
-
if (tier !== "free" && tier !== "pro") {
|
|
186
|
-
delete device.envVariables;
|
|
187
|
-
delete device.extraDeviceInfo;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const now = Date.now();
|
|
191
|
-
for (const device of allDevices) {
|
|
192
|
-
if (!device.deviceId) continue;
|
|
193
|
-
const instances = liveInstances(device.deviceId);
|
|
194
|
-
const collision = instances > 1;
|
|
195
|
-
if (collision && !device.collision) {
|
|
196
|
-
brokerLog(
|
|
197
|
-
"warn",
|
|
198
|
-
`${instances} apps are registered as "${device.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.`,
|
|
199
|
-
{ deviceId: device.deviceId, instances }
|
|
200
|
-
);
|
|
201
|
-
} else if (!collision && device.collision) {
|
|
202
|
-
brokerLog("info", `Device id collision cleared for "${device.deviceId}"`, {
|
|
203
|
-
deviceId: device.deviceId
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
device.collision = collision;
|
|
207
|
-
}
|
|
208
|
-
for (let i = allDevices.length - 1; i >= 0; i--) {
|
|
209
|
-
const device = allDevices[i];
|
|
210
|
-
if (!device.isConnected && device.lastSeenAt && now - device.lastSeenAt > MAX_OFFLINE_AGE_MS) {
|
|
211
|
-
allDevices.splice(i, 1);
|
|
212
|
-
if (device.deviceId) {
|
|
213
|
-
deviceCapabilities.delete(device.deviceId);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
io.emit(
|
|
218
|
-
"all-devices-update",
|
|
219
|
-
allDevices.filter((device) => device.deviceName !== "Dashboard").map(({ licenseKey: _key, ...device }) => device)
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
function handleClose(id, reason) {
|
|
223
|
-
const user = users.find((user2) => user2.id === id);
|
|
224
|
-
const deviceId = deviceIdMap.get(id);
|
|
225
|
-
deviceIdMap.delete(id);
|
|
226
|
-
instanceIdMap.delete(id);
|
|
227
|
-
socketTiers.delete(id);
|
|
228
|
-
let survivor;
|
|
229
|
-
if (deviceId) {
|
|
230
|
-
for (const [socketId, mappedDeviceId] of deviceIdMap) {
|
|
231
|
-
if (mappedDeviceId === deviceId) survivor = socketId;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (deviceId && survivor) {
|
|
235
|
-
const userIndex = users.findIndex((u) => u.deviceId === deviceId);
|
|
236
|
-
if (userIndex !== -1 && users[userIndex].id === id) {
|
|
237
|
-
users[userIndex].id = survivor;
|
|
238
|
-
}
|
|
239
|
-
const deviceIndex = allDevices.findIndex((d) => d.deviceId === deviceId);
|
|
240
|
-
if (deviceIndex !== -1 && allDevices[deviceIndex].id === id) {
|
|
241
|
-
allDevices[deviceIndex].id = survivor;
|
|
242
|
-
}
|
|
243
|
-
if (activeConnectionsMap.get(deviceId) === id) {
|
|
244
|
-
activeConnectionsMap.set(deviceId, survivor);
|
|
245
|
-
}
|
|
246
|
-
console.log(
|
|
247
|
-
`${LOG_PREFIX} Client disconnected - ID: ${id}, Name: ${user?.deviceName || "Unknown"} - Device ${deviceId} still connected via ${survivor}`
|
|
248
|
-
);
|
|
249
|
-
} else {
|
|
250
|
-
users = users.filter((user2) => user2.id !== id);
|
|
251
|
-
console.log(
|
|
252
|
-
`${LOG_PREFIX} Client disconnected - ID: ${id}, Name: ${user?.deviceName || "Unknown"} - Removing user record`
|
|
253
|
-
);
|
|
254
|
-
if (deviceId) {
|
|
255
|
-
const deviceIndex = allDevices.findIndex(
|
|
256
|
-
(u) => u.deviceId === deviceId
|
|
257
|
-
);
|
|
258
|
-
if (deviceIndex !== -1) {
|
|
259
|
-
allDevices[deviceIndex].isConnected = false;
|
|
260
|
-
allDevices[deviceIndex].lastDisconnectReason = reason;
|
|
261
|
-
allDevices[deviceIndex].lastSeenAt = Date.now();
|
|
262
|
-
activeConnectionsMap.delete(deviceId);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const wasDashboard = dashboardClients.includes(id);
|
|
267
|
-
dashboardClients = dashboardClients.filter((clientId) => clientId !== id);
|
|
268
|
-
releaseDashboardWatches(id);
|
|
269
|
-
const stillConnected = Boolean(deviceId && survivor);
|
|
270
|
-
brokerLog(
|
|
271
|
-
wasDashboard || stillConnected ? "debug" : "warn",
|
|
272
|
-
stillConnected ? `Socket closed for ${user?.deviceName || id} (${reason ?? "unknown"}) \u2014 device still connected via another socket` : `Disconnected: ${user?.deviceName || id} (${reason ?? "unknown"})`,
|
|
273
|
-
{ socketId: id, reason, wasDashboard, stillConnected }
|
|
274
|
-
);
|
|
275
|
-
emitAllDevices();
|
|
276
|
-
console.log(
|
|
277
|
-
`${LOG_PREFIX} Updated users list: ${users.map((u) => u.deviceName).join(", ")}`
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
function addNewUser({
|
|
281
|
-
id,
|
|
282
|
-
deviceName,
|
|
283
|
-
deviceId,
|
|
284
|
-
platform,
|
|
285
|
-
extraDeviceInfo,
|
|
286
|
-
envVariables,
|
|
287
|
-
connectionNonce
|
|
288
|
-
}) {
|
|
289
|
-
const isDashboard = deviceName === "Dashboard";
|
|
290
|
-
if (deviceId) {
|
|
291
|
-
deviceIdMap.set(id, deviceId);
|
|
292
|
-
activeConnectionsMap.set(deviceId, id);
|
|
293
|
-
const existingUserIndex = users.findIndex(
|
|
294
|
-
(user) => user.deviceId === deviceId
|
|
295
|
-
);
|
|
296
|
-
const existingDeviceIndex = allDevices.findIndex(
|
|
297
|
-
(device) => device.deviceId === deviceId
|
|
298
|
-
);
|
|
299
|
-
if (existingUserIndex !== -1) {
|
|
300
|
-
brokerLog(
|
|
301
|
-
"success",
|
|
302
|
-
`Reconnected: ${deviceName} (${platform || "unknown platform"})`,
|
|
303
|
-
{ socketId: id, deviceId }
|
|
304
|
-
);
|
|
305
|
-
users[existingUserIndex].id = id;
|
|
306
|
-
if (platform) {
|
|
307
|
-
users[existingUserIndex].platform = platform;
|
|
308
|
-
}
|
|
309
|
-
if (extraDeviceInfo) {
|
|
310
|
-
users[existingUserIndex].extraDeviceInfo = extraDeviceInfo;
|
|
311
|
-
}
|
|
312
|
-
if (envVariables) {
|
|
313
|
-
users[existingUserIndex].envVariables = envVariables;
|
|
314
|
-
}
|
|
315
|
-
users[existingUserIndex].isConnected = true;
|
|
316
|
-
users[existingUserIndex].connectionNonce = connectionNonce;
|
|
317
|
-
if (existingDeviceIndex !== -1) {
|
|
318
|
-
allDevices[existingDeviceIndex].id = id;
|
|
319
|
-
allDevices[existingDeviceIndex].isConnected = true;
|
|
320
|
-
allDevices[existingDeviceIndex].connectionNonce = connectionNonce;
|
|
321
|
-
allDevices[existingDeviceIndex].lastSeenAt = Date.now();
|
|
322
|
-
if (platform) {
|
|
323
|
-
allDevices[existingDeviceIndex].platform = platform;
|
|
324
|
-
}
|
|
325
|
-
if (extraDeviceInfo) {
|
|
326
|
-
allDevices[existingDeviceIndex].extraDeviceInfo = extraDeviceInfo;
|
|
327
|
-
}
|
|
328
|
-
if (envVariables) {
|
|
329
|
-
allDevices[existingDeviceIndex].envVariables = envVariables;
|
|
330
|
-
}
|
|
331
|
-
} else if (deviceName !== "Dashboard") {
|
|
332
|
-
allDevices.push({
|
|
333
|
-
id,
|
|
334
|
-
deviceName,
|
|
335
|
-
deviceId,
|
|
336
|
-
platform,
|
|
337
|
-
isConnected: true,
|
|
338
|
-
extraDeviceInfo,
|
|
339
|
-
envVariables,
|
|
340
|
-
lastSeenAt: Date.now()
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
emitAllDevices();
|
|
344
|
-
console.log(
|
|
345
|
-
`${LOG_PREFIX} Updated users list: ${users.map((u) => u.deviceName).join(", ")}`
|
|
346
|
-
);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
const extractNumber = (name) => {
|
|
351
|
-
const match = name.match(/(\d+)$/);
|
|
352
|
-
return match ? parseInt(match[1], 10) : 1;
|
|
353
|
-
};
|
|
354
|
-
let highestNumber = 0;
|
|
355
|
-
users.forEach((user) => {
|
|
356
|
-
if (user.deviceName.startsWith(deviceName)) {
|
|
357
|
-
const number = extractNumber(user.deviceName);
|
|
358
|
-
if (number >= highestNumber) {
|
|
359
|
-
highestNumber = number;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
if (highestNumber > 0 && !isDashboard) {
|
|
364
|
-
const renamed = `${deviceName} #${highestNumber + 1}`;
|
|
365
|
-
brokerLog(
|
|
366
|
-
"warn",
|
|
367
|
-
`Duplicate device name "${deviceName}" \u2014 renamed to "${renamed}"`,
|
|
368
|
-
{ socketId: id, deviceId }
|
|
369
|
-
);
|
|
370
|
-
deviceName = renamed;
|
|
371
|
-
}
|
|
372
|
-
if (id) {
|
|
373
|
-
const newUser = {
|
|
374
|
-
id,
|
|
375
|
-
deviceName,
|
|
376
|
-
deviceId,
|
|
377
|
-
platform,
|
|
378
|
-
isConnected: true,
|
|
379
|
-
extraDeviceInfo,
|
|
380
|
-
envVariables,
|
|
381
|
-
connectionNonce,
|
|
382
|
-
lastSeenAt: Date.now()
|
|
383
|
-
};
|
|
384
|
-
users.push(newUser);
|
|
385
|
-
if (deviceName !== "Dashboard" && deviceId) {
|
|
386
|
-
const existingDeviceIndex = allDevices.findIndex(
|
|
387
|
-
(device) => device.deviceId === deviceId
|
|
388
|
-
);
|
|
389
|
-
if (existingDeviceIndex !== -1) {
|
|
390
|
-
allDevices[existingDeviceIndex] = { ...newUser };
|
|
391
|
-
} else {
|
|
392
|
-
allDevices.push({ ...newUser });
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
brokerLog(
|
|
396
|
-
isDashboard ? "debug" : "info",
|
|
397
|
-
`Connected: ${deviceName}${platform ? ` (${platform})` : ""}`,
|
|
398
|
-
{ socketId: id, deviceId }
|
|
399
|
-
);
|
|
400
|
-
if (isDashboard && id) {
|
|
401
|
-
dashboardClients.push(id);
|
|
402
|
-
console.log(`${LOG_PREFIX} Registered dashboard client with ID: ${id}`);
|
|
403
|
-
}
|
|
404
|
-
emitAllDevices();
|
|
405
|
-
console.log(
|
|
406
|
-
`${LOG_PREFIX} Updated device list: ${users.map((u) => u.deviceName).join(", ")}`
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function getDeviceFromDeviceId(deviceId) {
|
|
411
|
-
return allDevices.find((device) => device.deviceId === deviceId);
|
|
412
|
-
}
|
|
413
|
-
function handleUserMessage(message, deviceName) {
|
|
414
|
-
console.log(`${LOG_PREFIX} Message from ${deviceName}: ${message}`);
|
|
415
|
-
if (!canUseDesktop()) return;
|
|
416
|
-
io.emit("message", `Device ${deviceName} sent message: ${message}`);
|
|
417
|
-
}
|
|
418
|
-
function forwardToDashboards(event, message) {
|
|
419
|
-
if (!canUseDesktop()) return;
|
|
420
|
-
dashboardClients = dashboardClients.filter(
|
|
421
|
-
(id) => io.sockets.sockets.has(id)
|
|
422
|
-
);
|
|
423
|
-
if (dashboardClients.length === 0) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
for (const dashboardId of dashboardClients) {
|
|
427
|
-
io.sockets.sockets.get(dashboardId)?.emit(event, message);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
function withTargetUsers(targetDeviceId, action, actionName) {
|
|
431
|
-
if (!canUseDesktop()) return;
|
|
432
|
-
const admitted = (user) => {
|
|
433
|
-
const tier = socketTiers.get(user.id);
|
|
434
|
-
return tier === "free" || tier === "pro";
|
|
435
|
-
};
|
|
436
|
-
const deviceName = getDeviceFromDeviceId(targetDeviceId)?.deviceName;
|
|
437
|
-
if (targetDeviceId === "No devices available") {
|
|
438
|
-
console.log(
|
|
439
|
-
`${LOG_PREFIX} Skipping ${actionName} - No devices available`
|
|
440
|
-
);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (targetDeviceId === "All") {
|
|
444
|
-
console.log(`${LOG_PREFIX} Broadcasting ${actionName} to all devices`);
|
|
445
|
-
const deviceUsers = users.filter(
|
|
446
|
-
(user) => user.deviceName !== "Dashboard"
|
|
447
|
-
);
|
|
448
|
-
console.log(
|
|
449
|
-
`${LOG_PREFIX} Sending to ${deviceUsers.length} devices: ${deviceUsers.map((u) => u.deviceName).join(", ")}`
|
|
450
|
-
);
|
|
451
|
-
deviceUsers.filter(admitted).forEach(action);
|
|
452
|
-
} else {
|
|
453
|
-
const targetUser = users.find((user) => user.deviceId === targetDeviceId);
|
|
454
|
-
if (targetUser && admitted(targetUser)) {
|
|
455
|
-
console.log(
|
|
456
|
-
`${LOG_PREFIX} Sending to target device ${targetUser.deviceName} (${targetUser.deviceId}) targetDeviceId: ${targetDeviceId}`
|
|
457
|
-
);
|
|
458
|
-
action(targetUser);
|
|
459
|
-
} else {
|
|
460
|
-
console.log(
|
|
461
|
-
`${LOG_PREFIX} Target device not found - DeviceId: ${targetDeviceId}, DeviceName: ${deviceName}`
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
function emitWatchState(key, mode) {
|
|
467
|
-
const [targetDeviceId, toolId] = fromWatchKey(key);
|
|
468
|
-
const message = mode === null ? { toolId, targetDeviceId, watching: false, consolidated: true } : {
|
|
469
|
-
toolId,
|
|
470
|
-
targetDeviceId,
|
|
471
|
-
watching: true,
|
|
472
|
-
stream: mode === "stream",
|
|
473
|
-
consolidated: true
|
|
474
|
-
};
|
|
475
|
-
withTargetUsers(
|
|
476
|
-
targetDeviceId,
|
|
477
|
-
(user) => io.to(user.id).emit("devtool-watch", message),
|
|
478
|
-
`devtool ${mode === null ? "unwatch" : `watch(${mode})`} ${toolId}`
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
function handleDevtoolWatch(dashboardId, message) {
|
|
482
|
-
const key = toWatchKey(message.targetDeviceId, message.toolId);
|
|
483
|
-
if (message.watching) {
|
|
484
|
-
const mode2 = message.stream === false ? "capture" : "stream";
|
|
485
|
-
const effective = watches.watch(dashboardId, key, mode2);
|
|
486
|
-
console.log(
|
|
487
|
-
`${LOG_PREFIX} Dashboard ${dashboardId} watching "${message.toolId}" on ${message.targetDeviceId} (${mode2}, effective ${effective})`
|
|
488
|
-
);
|
|
489
|
-
emitWatchState(key, effective);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const { changed, mode } = watches.unwatch(dashboardId, key);
|
|
493
|
-
if (!changed) return;
|
|
494
|
-
console.log(
|
|
495
|
-
mode === null ? `${LOG_PREFIX} Last dashboard stopped watching "${message.toolId}" on ${message.targetDeviceId}` : `${LOG_PREFIX} "${message.toolId}" on ${message.targetDeviceId} downgraded to ${mode}`
|
|
496
|
-
);
|
|
497
|
-
emitWatchState(key, mode);
|
|
498
|
-
}
|
|
499
|
-
function releaseDashboardWatches(dashboardId) {
|
|
500
|
-
for (const { key, mode } of watches.releaseDashboard(dashboardId)) {
|
|
501
|
-
emitWatchState(key, mode);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
io.on("connection", (socket) => {
|
|
505
|
-
const sendAccess = () => socket.emit("buoy:access", { protocol: 1, ready: canUseDesktop() });
|
|
506
|
-
sendAccess();
|
|
507
|
-
socket.on("buoy:access:request", sendAccess);
|
|
508
|
-
socket.use((_packet, next) => {
|
|
509
|
-
if (_packet[0] === "buoy:access:request") {
|
|
510
|
-
next();
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (!canUseDesktop()) {
|
|
514
|
-
sendAccess();
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const control = _packet[0] === "device-license";
|
|
518
|
-
if (control) {
|
|
519
|
-
const payload = _packet[1] ?? {};
|
|
520
|
-
socketTiers.set(socket.id, tierFromLicensePayload(payload));
|
|
521
|
-
}
|
|
522
|
-
const deviceAdmitted = socketTiers.get(socket.id) === "free" || socketTiers.get(socket.id) === "pro";
|
|
523
|
-
if (control || dashboardClients.includes(socket.id) || deviceAdmitted) next();
|
|
524
|
-
else sendAccess();
|
|
525
|
-
});
|
|
526
|
-
const {
|
|
527
|
-
deviceName,
|
|
528
|
-
deviceId,
|
|
529
|
-
platform,
|
|
530
|
-
extraDeviceInfo,
|
|
531
|
-
envVariables,
|
|
532
|
-
connectionNonce,
|
|
533
|
-
instanceId
|
|
534
|
-
} = socket.handshake.query;
|
|
535
|
-
if (typeof instanceId === "string" && instanceId) {
|
|
536
|
-
instanceIdMap.set(socket.id, instanceId);
|
|
537
|
-
}
|
|
538
|
-
console.log(
|
|
539
|
-
`${LOG_PREFIX} New connection - ID: ${socket.id}, Name: ${deviceName || "Unknown Device"}${deviceId ? `, DeviceId: ${deviceId}` : ""}${platform ? `, Platform: ${platform}` : ""}`
|
|
540
|
-
);
|
|
541
|
-
addNewUser({
|
|
542
|
-
id: socket.id,
|
|
543
|
-
deviceName: deviceName || "Unknown Device Name",
|
|
544
|
-
deviceId,
|
|
545
|
-
platform,
|
|
546
|
-
extraDeviceInfo: void 0,
|
|
547
|
-
envVariables: void 0,
|
|
548
|
-
connectionNonce
|
|
549
|
-
});
|
|
550
|
-
if (canUseDesktop() && deviceName === "Dashboard") {
|
|
551
|
-
for (const capabilities of deviceCapabilities.values()) {
|
|
552
|
-
socket.emit("devtool-capabilities", capabilities);
|
|
553
|
-
}
|
|
554
|
-
for (const entry of brokerLogBuffer) {
|
|
555
|
-
socket.emit("broker-log", entry);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
socket.on(
|
|
559
|
-
"device-license",
|
|
560
|
-
(payload) => {
|
|
561
|
-
const nextTier = tierFromLicensePayload(payload ?? {});
|
|
562
|
-
const nextIsPro = nextTier === "pro";
|
|
563
|
-
socketTiers.set(socket.id, nextTier);
|
|
564
|
-
const user = users.find((u) => u.id === socket.id);
|
|
565
|
-
if (user) {
|
|
566
|
-
user.isPro = nextIsPro;
|
|
567
|
-
user.tier = nextTier;
|
|
568
|
-
delete user.licenseKey;
|
|
569
|
-
}
|
|
570
|
-
const did = user?.deviceId || deviceIdMap.get(socket.id);
|
|
571
|
-
if (nextTier !== "locked") {
|
|
572
|
-
for (const record of [...users, ...allDevices].filter((record2) => record2.id === socket.id)) {
|
|
573
|
-
record.extraDeviceInfo = redactAccountKeys(extraDeviceInfo);
|
|
574
|
-
record.envVariables = redactAccountKeys(envVariables);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
if (did) {
|
|
578
|
-
if (nextTier === "locked") deviceCapabilities.delete(did);
|
|
579
|
-
const device = allDevices.find((d) => d.deviceId === did);
|
|
580
|
-
if (device) {
|
|
581
|
-
device.isPro = nextIsPro;
|
|
582
|
-
device.tier = nextTier;
|
|
583
|
-
delete device.licenseKey;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
brokerLog(
|
|
587
|
-
"debug",
|
|
588
|
-
`device-license from ${user?.deviceName || socket.id}: tier=${nextTier}`,
|
|
589
|
-
{ socketId: socket.id }
|
|
590
|
-
);
|
|
591
|
-
emitAllDevices();
|
|
592
|
-
}
|
|
593
|
-
);
|
|
594
|
-
socket.on(
|
|
595
|
-
"disconnect",
|
|
596
|
-
(reason) => handleClose(socket.id, reason)
|
|
597
|
-
);
|
|
598
|
-
socket.on("message", (msg) => {
|
|
599
|
-
handleUserMessage(msg, deviceName || "Unknown");
|
|
600
|
-
});
|
|
601
|
-
socket.on("devtool-capabilities", (message) => {
|
|
602
|
-
if (!message || !Array.isArray(message.tools) || message.persistentDeviceId !== deviceIdMap.get(socket.id)) return;
|
|
603
|
-
console.log(
|
|
604
|
-
`${LOG_PREFIX} Capabilities from ${message.persistentDeviceId}: ${message.tools.map((tool) => `${tool.toolId}@v${tool.version}`).join(", ")}`
|
|
605
|
-
);
|
|
606
|
-
if (message.protocolVersion !== SYNC_PROTOCOL_VERSION) {
|
|
607
|
-
brokerLog(
|
|
608
|
-
"warn",
|
|
609
|
-
`Protocol version mismatch from ${message.persistentDeviceId}: device speaks v${message.protocolVersion}, broker speaks v${SYNC_PROTOCOL_VERSION} \u2014 update @buoy-gg packages or Buoy Desktop`,
|
|
610
|
-
{ deviceId: message.persistentDeviceId }
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
deviceCapabilities.set(message.persistentDeviceId, message);
|
|
614
|
-
forwardToDashboards("devtool-capabilities", message);
|
|
615
|
-
for (const key of watches.activeKeys()) {
|
|
616
|
-
const [targetDeviceId, toolId] = fromWatchKey(key);
|
|
617
|
-
if (targetDeviceId !== "All" && targetDeviceId !== message.persistentDeviceId) {
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
const mode = watches.effective(key);
|
|
621
|
-
if (!mode) continue;
|
|
622
|
-
socket.emit("devtool-watch", {
|
|
623
|
-
toolId,
|
|
624
|
-
targetDeviceId,
|
|
625
|
-
watching: true,
|
|
626
|
-
stream: mode === "stream",
|
|
627
|
-
consolidated: true
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
socket.on("devtool-sync", (message) => {
|
|
632
|
-
if (!message || message.persistentDeviceId !== deviceIdMap.get(socket.id)) return;
|
|
633
|
-
forwardToDashboards("devtool-sync", message);
|
|
634
|
-
});
|
|
635
|
-
socket.on(
|
|
636
|
-
"devtool-action-result",
|
|
637
|
-
(message) => {
|
|
638
|
-
if (!message || message.persistentDeviceId !== deviceIdMap.get(socket.id)) return;
|
|
639
|
-
console.log(
|
|
640
|
-
`${LOG_PREFIX} Action result from ${message.persistentDeviceId}: ${message.toolId}.${message.action} -> ${message.ok ? "ok" : `error: ${message.error}`}`
|
|
641
|
-
);
|
|
642
|
-
forwardToDashboards("devtool-action-result", message);
|
|
643
|
-
}
|
|
644
|
-
);
|
|
645
|
-
socket.on("devtool-watch", (message) => {
|
|
646
|
-
handleDevtoolWatch(socket.id, message);
|
|
647
|
-
});
|
|
648
|
-
socket.on("request-devtool-state", (message) => {
|
|
649
|
-
withTargetUsers(
|
|
650
|
-
message.targetDeviceId,
|
|
651
|
-
(user) => io.to(user.id).emit("request-devtool-state", message),
|
|
652
|
-
`devtool state request ${message.toolId}`
|
|
653
|
-
);
|
|
654
|
-
});
|
|
655
|
-
socket.on("forget-device", (payload) => {
|
|
656
|
-
const targetDeviceId = payload?.deviceId;
|
|
657
|
-
if (!dashboardClients.includes(socket.id)) return;
|
|
658
|
-
if (!targetDeviceId || activeConnectionsMap.has(targetDeviceId)) return;
|
|
659
|
-
const target = allDevices.find((d) => d.deviceId === targetDeviceId);
|
|
660
|
-
users = users.filter((u) => u.deviceId !== targetDeviceId);
|
|
661
|
-
const historyIndex = allDevices.findIndex(
|
|
662
|
-
(d) => d.deviceId === targetDeviceId
|
|
663
|
-
);
|
|
664
|
-
if (historyIndex !== -1) {
|
|
665
|
-
allDevices.splice(historyIndex, 1);
|
|
666
|
-
}
|
|
667
|
-
deviceCapabilities.delete(targetDeviceId);
|
|
668
|
-
for (const [socketId, mappedDeviceId] of deviceIdMap) {
|
|
669
|
-
if (mappedDeviceId === targetDeviceId) {
|
|
670
|
-
deviceIdMap.delete(socketId);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
brokerLog(
|
|
674
|
-
"info",
|
|
675
|
-
`Forgot device: ${target?.deviceName ?? targetDeviceId}`,
|
|
676
|
-
{ deviceId: targetDeviceId }
|
|
677
|
-
);
|
|
678
|
-
emitAllDevices();
|
|
679
|
-
});
|
|
680
|
-
socket.on("devtool-action", (message) => {
|
|
681
|
-
console.log(
|
|
682
|
-
`${LOG_PREFIX} Devtool action from dashboard: ${message.toolId}.${message.action} -> ${message.targetDeviceId}`
|
|
683
|
-
);
|
|
684
|
-
withTargetUsers(
|
|
685
|
-
message.targetDeviceId,
|
|
686
|
-
(user) => io.to(user.id).emit("devtool-action", message),
|
|
687
|
-
`devtool action ${message.toolId}.${message.action}`
|
|
688
|
-
);
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
return {
|
|
692
|
-
refreshAccess() {
|
|
693
|
-
const ready = canUseDesktop();
|
|
694
|
-
if (!ready) {
|
|
695
|
-
socketTiers.clear();
|
|
696
|
-
deviceCapabilities.clear();
|
|
697
|
-
brokerLogBuffer.length = 0;
|
|
698
|
-
watches = new WatchRegistry();
|
|
699
|
-
for (const device of [...users, ...allDevices]) {
|
|
700
|
-
delete device.licenseKey;
|
|
701
|
-
delete device.envVariables;
|
|
702
|
-
delete device.extraDeviceInfo;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
io.emit("buoy:access", { protocol: 1, ready });
|
|
706
|
-
emitAllDevices();
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// src/createBroker.ts
|
|
712
|
-
var import_http = __toESM(require("http"));
|
|
713
|
-
var import_socket = require("socket.io");
|
|
714
|
-
var DEFAULT_BROKER_PORT = 42831;
|
|
715
|
-
var SOCKET_CONFIG = {
|
|
716
|
-
cors: {
|
|
717
|
-
origin: "*",
|
|
718
|
-
methods: ["GET", "POST"]
|
|
719
|
-
},
|
|
720
|
-
// Dev-tool snapshots are whole app-state dumps (a dehydrated React Query
|
|
721
|
-
// cache, a Redux/Zustand store, an aggregated event log, etc.) and a device
|
|
722
|
-
// pushes one per watched tool on connect. socket.io v3+ caps inbound messages
|
|
723
|
-
// at 1 MB by default and TERMINATES the connection ("transport error") when a
|
|
724
|
-
// single message exceeds it — which, with a dashboard watching all ~11 tools
|
|
725
|
-
// at once, makes a device's connection flap. These are local, trusted dev
|
|
726
|
-
// connections, so lift the cap well clear of any realistic state dump.
|
|
727
|
-
maxHttpBufferSize: 1e8,
|
|
728
|
-
// 100 MB
|
|
729
|
-
transports: ["websocket", "polling"],
|
|
730
|
-
pingTimeout: 3e4,
|
|
731
|
-
pingInterval: 25e3
|
|
732
|
-
};
|
|
733
|
-
function createBroker(options = {}) {
|
|
734
|
-
const port = options.port ?? DEFAULT_BROKER_PORT;
|
|
735
|
-
const httpServer = import_http.default.createServer((_req, res) => {
|
|
736
|
-
res.writeHead(200, { "content-type": "text/plain" });
|
|
737
|
-
res.end(`Buoy sync broker running on port ${port}`);
|
|
738
|
-
});
|
|
739
|
-
const io = new import_socket.Server(httpServer, {
|
|
740
|
-
...SOCKET_CONFIG,
|
|
741
|
-
...options.socketOptions
|
|
742
|
-
});
|
|
743
|
-
const routing = socketHandle({ io, canUseDesktop: options.canUseDesktop });
|
|
744
|
-
httpServer.on("error", (error) => {
|
|
745
|
-
options.onError?.(error);
|
|
746
|
-
});
|
|
747
|
-
httpServer.listen(port, () => {
|
|
748
|
-
options.onListening?.(port);
|
|
749
|
-
});
|
|
750
|
-
const close = () => new Promise((resolve) => {
|
|
751
|
-
io.close(() => {
|
|
752
|
-
httpServer.close(() => resolve());
|
|
753
|
-
});
|
|
754
|
-
});
|
|
755
|
-
return { io, httpServer, port, close, refreshAccess: routing.refreshAccess };
|
|
756
|
-
}
|
|
757
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
758
|
-
0 && (module.exports = {
|
|
759
|
-
DEFAULT_BROKER_PORT,
|
|
760
|
-
SOCKET_CONFIG,
|
|
761
|
-
SYNC_PROTOCOL_VERSION,
|
|
762
|
-
createBroker,
|
|
763
|
-
socketHandle
|
|
764
|
-
});
|
|
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}}
|