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