@buoy-gg/core 7.0.21 → 7.0.23
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/bin/__tests__/buoyCli.test.ts +131 -0
- package/bin/buoy.js +299 -0
- package/lib/commonjs/Buoy.js +14 -0
- package/lib/commonjs/floatingMenu/FloatingDevTools.js +48 -10
- package/lib/commonjs/floatingMenu/ToolHost.js +5 -1
- package/lib/commonjs/floatingMenu/autoExternalSync.js +13 -7
- package/lib/commonjs/floatingMenu/externalSyncGate.js +49 -0
- package/lib/commonjs/floatingMenu/sync/appSyncAdapter.js +15 -2
- package/lib/commonjs/telemetry/installPing.js +124 -0
- package/lib/commonjs/version.js +18 -0
- package/lib/module/Buoy.js +13 -0
- package/lib/module/floatingMenu/FloatingDevTools.js +48 -10
- package/lib/module/floatingMenu/ToolHost.js +5 -1
- package/lib/module/floatingMenu/autoExternalSync.js +13 -7
- package/lib/module/floatingMenu/externalSyncGate.js +44 -0
- package/lib/module/floatingMenu/sync/appSyncAdapter.js +16 -3
- package/lib/module/telemetry/installPing.js +120 -0
- package/lib/module/version.js +14 -0
- package/lib/typescript/commonjs/Buoy.d.ts +14 -0
- package/lib/typescript/commonjs/Buoy.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +15 -0
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts +7 -1
- package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +28 -2
- package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/externalSyncGate.d.ts +30 -0
- package/lib/typescript/commonjs/floatingMenu/externalSyncGate.d.ts.map +1 -0
- package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts +2 -0
- package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/commonjs/telemetry/installPing.d.ts +37 -0
- package/lib/typescript/commonjs/telemetry/installPing.d.ts.map +1 -0
- package/lib/typescript/commonjs/version.d.ts +13 -0
- package/lib/typescript/commonjs/version.d.ts.map +1 -0
- package/lib/typescript/module/Buoy.d.ts +14 -0
- package/lib/typescript/module/Buoy.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +15 -0
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/ToolHost.d.ts +7 -1
- package/lib/typescript/module/floatingMenu/ToolHost.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +28 -2
- package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/externalSyncGate.d.ts +30 -0
- package/lib/typescript/module/floatingMenu/externalSyncGate.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts +2 -0
- package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/module/telemetry/installPing.d.ts +37 -0
- package/lib/typescript/module/telemetry/installPing.d.ts.map +1 -0
- package/lib/typescript/module/version.d.ts +13 -0
- package/lib/typescript/module/version.d.ts.map +1 -0
- package/package.json +9 -5
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.externalSyncOptions = externalSyncOptions;
|
|
7
|
+
exports.shouldMountExternalSync = shouldMountExternalSync;
|
|
8
|
+
/**
|
|
9
|
+
* Decides whether FloatingDevTools mounts <AutoExternalSync /> — i.e. whether
|
|
10
|
+
* this build is allowed to dial the Buoy Desktop broker.
|
|
11
|
+
*
|
|
12
|
+
* Kept as a pure function in its own module (no react-native / no guarded
|
|
13
|
+
* requires) so the rule is unit-testable: the gate used to be an inline
|
|
14
|
+
* `isDevelopment && ...` in JSX, which silently made release builds unable to
|
|
15
|
+
* reach the desktop dashboard or MCP with no way to opt in from app code.
|
|
16
|
+
*
|
|
17
|
+
* The rule has two halves that must BOTH hold in a release build, because a
|
|
18
|
+
* shipped app must never contact a broker on an end user's device:
|
|
19
|
+
* 1. a real Pro license (never the Weekend Pass — see useProAccess), and
|
|
20
|
+
* 2. an EXPLICIT opt-in from app code (`externalSync.enableInRelease`).
|
|
21
|
+
* Dev builds keep the zero-config behavior: sync is on unless turned off.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The options object from the prop, or undefined when it's a boolean. */
|
|
25
|
+
function externalSyncOptions(externalSync) {
|
|
26
|
+
return typeof externalSync === "object" && externalSync !== null ? externalSync : undefined;
|
|
27
|
+
}
|
|
28
|
+
function shouldMountExternalSync({
|
|
29
|
+
isDevelopment,
|
|
30
|
+
isLicensed,
|
|
31
|
+
externalSync,
|
|
32
|
+
isAvailable
|
|
33
|
+
}) {
|
|
34
|
+
// Turned off outright, or the package isn't installed — nothing to mount.
|
|
35
|
+
if (externalSync === false) return false;
|
|
36
|
+
if (!isAvailable) return false;
|
|
37
|
+
|
|
38
|
+
// Dev builds: zero-config, on by default (historical behavior).
|
|
39
|
+
if (isDevelopment) return true;
|
|
40
|
+
|
|
41
|
+
// Release builds: opt-in only, and Pro only. `enableInRelease` has to be set
|
|
42
|
+
// by the app itself, so an app that just ships FloatingDevTools with a Pro
|
|
43
|
+
// key still never opens a socket on a customer's phone.
|
|
44
|
+
//
|
|
45
|
+
// The `isLicensed` half is belt-and-braces: FloatingDevTools already renders
|
|
46
|
+
// nothing at all for an unlicensed release build. Keep it anyway so this rule
|
|
47
|
+
// stands on its own and can't be broken by a change to that earlier return.
|
|
48
|
+
return isLicensed && externalSyncOptions(externalSync)?.enableInRelease === true;
|
|
49
|
+
}
|
|
@@ -27,12 +27,24 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
27
27
|
* and reporting it too early.
|
|
28
28
|
*/
|
|
29
29
|
const REALM_STARTED_AT = Date.now();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Where this bundle came from, resolved once at module load.
|
|
33
|
+
*
|
|
34
|
+
* Reported so a remote driver can recover the app when the app itself can no
|
|
35
|
+
* longer be asked to: after a fatal render error the React tree is gone, every
|
|
36
|
+
* tool action goes unanswered, and `reloadApp` below is unreachable. The dev
|
|
37
|
+
* server's reload path runs natively and still works — but only for a driver
|
|
38
|
+
* that learned this origin while the app was healthy.
|
|
39
|
+
*/
|
|
40
|
+
const DEV_SERVER_ORIGIN = (0, _sharedUi.getDevServerOrigin)();
|
|
30
41
|
const appSyncAdapter = exports.appSyncAdapter = {
|
|
31
|
-
version:
|
|
42
|
+
version: 2,
|
|
32
43
|
getSnapshot: () => {
|
|
33
44
|
const strategy = (0, _sharedUi.resolveReloadStrategy)("auto");
|
|
34
45
|
return {
|
|
35
46
|
startedAt: REALM_STARTED_AT,
|
|
47
|
+
devServerOrigin: DEV_SERVER_ORIGIN,
|
|
36
48
|
reload: {
|
|
37
49
|
available: strategy !== null,
|
|
38
50
|
strategy
|
|
@@ -50,7 +62,8 @@ const appSyncAdapter = exports.appSyncAdapter = {
|
|
|
50
62
|
*/
|
|
51
63
|
ping: () => ({
|
|
52
64
|
ok: true,
|
|
53
|
-
startedAt: REALM_STARTED_AT
|
|
65
|
+
startedAt: REALM_STARTED_AT,
|
|
66
|
+
devServerOrigin: DEV_SERVER_ORIGIN
|
|
54
67
|
}),
|
|
55
68
|
/**
|
|
56
69
|
* Reload the app's JS bundle: RN's `DevSettings.reload()` in dev builds,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.__resetInstallPingForTests = __resetInstallPingForTests;
|
|
7
|
+
exports.sendInstallPing = sendInstallPing;
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
10
|
+
var _version = require("../version.js");
|
|
11
|
+
/**
|
|
12
|
+
* Anonymous install ping.
|
|
13
|
+
*
|
|
14
|
+
* Answers the one question licensing data cannot: how many people run Buoy at
|
|
15
|
+
* all. Keygen sees every free and paid key, but by definition it cannot see an
|
|
16
|
+
* install that never claimed one — so without this, "what fraction of installs
|
|
17
|
+
* have an account" has no denominator.
|
|
18
|
+
*
|
|
19
|
+
* Constraints this is built to honor:
|
|
20
|
+
*
|
|
21
|
+
* - **Development only.** Never fires from a shipped app. Buoy is a developer
|
|
22
|
+
* tool; pinging from a release build would phone home from an end user's
|
|
23
|
+
* device — someone who never chose to use Buoy — which is not a trade we are
|
|
24
|
+
* willing to make for a metric.
|
|
25
|
+
* - **No PII, no fingerprint.** The install id is a random UUID minted here.
|
|
26
|
+
* It is not derived from hardware, account, project, or app identity, so it
|
|
27
|
+
* cannot be correlated to a person or joined against anything else we hold.
|
|
28
|
+
* - **No tool usage.** What you open and inspect stays on your machine, which
|
|
29
|
+
* is the whole security story for enterprise adoption. Only the four fields
|
|
30
|
+
* below ever leave.
|
|
31
|
+
* - **Silent and harmless.** Fire-and-forget, throttled to once a day, and
|
|
32
|
+
* every failure path is swallowed. It can never block, slow, or break an app.
|
|
33
|
+
*
|
|
34
|
+
* Opt out with `Buoy.init({ telemetry: false })`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Same `@react_buoy` prefix as every other Buoy key, so it is inspectable. */
|
|
38
|
+
const STORAGE_KEY = "@react_buoy_install";
|
|
39
|
+
const ENDPOINT = "https://buoy.gg/api/t";
|
|
40
|
+
|
|
41
|
+
/** One ping per install per day is enough to count installs and daily actives. */
|
|
42
|
+
const PING_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
43
|
+
/** RFC4122 v4, from Math.random — this is a counter, not a credential. */
|
|
44
|
+
function uuid() {
|
|
45
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
46
|
+
const r = Math.random() * 16 | 0;
|
|
47
|
+
const v = c === "x" ? r : r & 0x3 | 0x8;
|
|
48
|
+
return v.toString(16);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function readState() {
|
|
52
|
+
try {
|
|
53
|
+
const raw = await _sharedUi.persistentStorage.getItem(STORAGE_KEY);
|
|
54
|
+
if (!raw) return null;
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (typeof parsed?.installId !== "string") return null;
|
|
57
|
+
return {
|
|
58
|
+
installId: parsed.installId,
|
|
59
|
+
lastPingAt: typeof parsed.lastPingAt === "number" ? parsed.lastPingAt : 0
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Told once, on the very first ping of a new install — not on every launch.
|
|
68
|
+
*
|
|
69
|
+
* A dev tool that phones home silently deserves the reputation it would get.
|
|
70
|
+
* One line, the first time, saying exactly what leaves and how to stop it.
|
|
71
|
+
*/
|
|
72
|
+
function announceOnce() {
|
|
73
|
+
console.log("[Buoy] Anonymous usage ping enabled: a random install id, the Buoy " + "version, your platform, and your license tier — no project, app, or " + "tool data, and only in development. Opt out with " + "Buoy.init({ telemetry: false }). Details: https://buoy.gg/docs/telemetry");
|
|
74
|
+
}
|
|
75
|
+
let sentThisSession = false;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Send the ping if it is due. Safe to call on every mount: it is idempotent
|
|
79
|
+
* within a session, throttled to once a day across sessions, and resolves
|
|
80
|
+
* without throwing no matter what fails.
|
|
81
|
+
*/
|
|
82
|
+
async function sendInstallPing(options) {
|
|
83
|
+
// Never from a shipped app — see the header. This is the single most
|
|
84
|
+
// important line in the file.
|
|
85
|
+
const isDevelopment = typeof __DEV__ !== "undefined" && __DEV__;
|
|
86
|
+
if (!isDevelopment) return;
|
|
87
|
+
if (!options.enabled) return;
|
|
88
|
+
if (sentThisSession) return;
|
|
89
|
+
sentThisSession = true;
|
|
90
|
+
try {
|
|
91
|
+
const existing = await readState();
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (existing && now - existing.lastPingAt < PING_INTERVAL_MS) return;
|
|
94
|
+
const installId = existing?.installId ?? uuid();
|
|
95
|
+
if (!existing) announceOnce();
|
|
96
|
+
|
|
97
|
+
// Recorded BEFORE the request, so a server that is down or unreachable
|
|
98
|
+
// can't turn into a ping every single mount.
|
|
99
|
+
await _sharedUi.persistentStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
100
|
+
installId,
|
|
101
|
+
lastPingAt: now
|
|
102
|
+
}));
|
|
103
|
+
await fetch(ENDPOINT, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "application/json"
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
installId,
|
|
110
|
+
version: _version.SDK_VERSION,
|
|
111
|
+
platform: _reactNative.Platform.OS,
|
|
112
|
+
tier: options.tier
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
// Offline, firewalled, endpoint down — all fine. A metric is never worth
|
|
117
|
+
// a warning in someone else's console.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Test seam: forget that a ping already went out this session. */
|
|
122
|
+
function __resetInstallPingForTests() {
|
|
123
|
+
sentThisSession = false;
|
|
124
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SDK_VERSION = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* This package's published version.
|
|
9
|
+
*
|
|
10
|
+
* Hardcoded rather than read from package.json: `require("../package.json")`
|
|
11
|
+
* resolves to a different depth from `src/` than from the built `lib/`, and
|
|
12
|
+
* react-native-builder-bob (babel) has no build-time `define` the way tsup does
|
|
13
|
+
* for @buoy-gg/mcp.
|
|
14
|
+
*
|
|
15
|
+
* Drift is prevented by a test that fails when this disagrees with
|
|
16
|
+
* package.json, so a version bump that forgets this file cannot ship.
|
|
17
|
+
*/
|
|
18
|
+
const SDK_VERSION = exports.SDK_VERSION = "7.0.22";
|
package/lib/module/Buoy.js
CHANGED
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { LicenseManager } from "@buoy-gg/license";
|
|
19
|
+
/**
|
|
20
|
+
* Telemetry stays on unless an app explicitly opts out. Read at ping time
|
|
21
|
+
* rather than captured, so `Buoy.init()` still wins if it runs after mount.
|
|
22
|
+
*/
|
|
23
|
+
let telemetryEnabled = true;
|
|
24
|
+
|
|
25
|
+
/** Whether the anonymous install ping is allowed to fire. */
|
|
26
|
+
export function isTelemetryEnabled() {
|
|
27
|
+
return telemetryEnabled;
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* Buoy SDK namespace
|
|
21
32
|
*
|
|
@@ -44,6 +55,8 @@ export const Buoy = {
|
|
|
44
55
|
* ```
|
|
45
56
|
*/
|
|
46
57
|
init(config) {
|
|
58
|
+
if (config.telemetry === false) telemetryEnabled = false;
|
|
59
|
+
|
|
47
60
|
// If no license key or empty string, clear any cached license
|
|
48
61
|
if (!config.licenseKey || config.licenseKey.trim() === "") {
|
|
49
62
|
LicenseManager.initialize().then(() => LicenseManager.clearLicense()).catch(() => {});
|
|
@@ -6,6 +6,8 @@ import { AppHostProvider } from "./AppHost";
|
|
|
6
6
|
import { FloatingMenu } from "./FloatingMenu.js";
|
|
7
7
|
import { AppOverlay } from "./AppHost";
|
|
8
8
|
import { autoDiscoverPresets, autoDiscoverPresetsWithCustom } from "./autoDiscoverPresets.js";
|
|
9
|
+
import { sendInstallPing } from "../telemetry/installPing.js";
|
|
10
|
+
import { isTelemetryEnabled } from "../Buoy.js";
|
|
9
11
|
import { renderAppIcon } from "./AppIcon.js";
|
|
10
12
|
import { DevToolsVisibilityProvider } from "./DevToolsVisibilityContext.js";
|
|
11
13
|
import { reportOptionalRequireError } from "./optionalRequire.js";
|
|
@@ -15,6 +17,7 @@ import { validateDialConfig } from "./defaultConfig.js";
|
|
|
15
17
|
import { DefaultConfigProvider } from "./DefaultConfigContext.js";
|
|
16
18
|
import { LicenseManager, useProAccess, useLicense } from "@buoy-gg/license";
|
|
17
19
|
import { AutoExternalSync, isExternalSyncAvailable } from "./autoExternalSync.js";
|
|
20
|
+
import { externalSyncOptions, shouldMountExternalSync } from "./externalSyncGate.js";
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Environment variable configuration
|
|
@@ -129,8 +132,20 @@ export const FloatingDevTools = ({
|
|
|
129
132
|
// Get full license state for requireLicense gating
|
|
130
133
|
const {
|
|
131
134
|
licenseKey,
|
|
132
|
-
isInitialized
|
|
135
|
+
isInitialized,
|
|
136
|
+
tier
|
|
133
137
|
} = useLicense();
|
|
138
|
+
|
|
139
|
+
// Anonymous install ping — dev-only, once a day, no project or tool data.
|
|
140
|
+
// Deliberately waits for the license to resolve so the tier it reports is
|
|
141
|
+
// the real one rather than the "anonymous" the manager starts at.
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!isInitialized) return;
|
|
144
|
+
void sendInstallPing({
|
|
145
|
+
tier,
|
|
146
|
+
enabled: isTelemetryEnabled()
|
|
147
|
+
});
|
|
148
|
+
}, [isInitialized, tier]);
|
|
134
149
|
const [showLicenseModal, setShowLicenseModal] = useState(false);
|
|
135
150
|
|
|
136
151
|
// Initialize license manager on mount, and set/clear license key when prop changes.
|
|
@@ -375,25 +390,47 @@ export const FloatingDevTools = ({
|
|
|
375
390
|
});
|
|
376
391
|
}, [finalApps]);
|
|
377
392
|
|
|
378
|
-
// Gate:
|
|
379
|
-
//
|
|
393
|
+
// Gate: only a PAID license works outside development.
|
|
394
|
+
//
|
|
395
|
+
// Both untiered surfaces below it — anonymous (no key) and free (a key on the
|
|
396
|
+
// Free policy) — stay dev-only. `isLicensed` is safe to gate on because
|
|
397
|
+
// `isPro` is a strict mirror of `tier === "pro"`, so a free key reports false
|
|
398
|
+
// here even though it validates perfectly well.
|
|
399
|
+
//
|
|
400
|
+
// When Business on-device login lands, this becomes the boundary that a
|
|
401
|
+
// logged-in QA/support seat crosses WITHOUT a key in the build — i.e.
|
|
402
|
+
// `isLicensed || tierSource === "session"`. Nothing else should.
|
|
380
403
|
const isDevelopment = typeof __DEV__ !== "undefined" && __DEV__;
|
|
381
404
|
if (!isDevelopment && !isLicensed) {
|
|
382
|
-
//
|
|
405
|
+
// Anonymous or free user in production - don't render DevTools.
|
|
406
|
+
// NOTE for whoever relaxes this for the free tier: desktop sync does NOT
|
|
407
|
+
// ride on this check. It re-tests `isLicensed` itself (externalSyncGate),
|
|
408
|
+
// so opening this door does not open a broker socket. Keep it that way.
|
|
383
409
|
return null;
|
|
384
410
|
}
|
|
385
411
|
|
|
412
|
+
// Zero-config in dev; in a release bundle the app must opt in explicitly
|
|
413
|
+
// (externalSync.enableInRelease) AND hold a real Pro license — see
|
|
414
|
+
// externalSyncGate.ts for why both halves are required.
|
|
415
|
+
const syncEnabled = shouldMountExternalSync({
|
|
416
|
+
isDevelopment,
|
|
417
|
+
isLicensed,
|
|
418
|
+
externalSync,
|
|
419
|
+
isAvailable: isExternalSyncAvailable()
|
|
420
|
+
});
|
|
421
|
+
|
|
386
422
|
// Headless (sync-only): tool adapters + route tracking + desktop sync, no
|
|
387
423
|
// on-device UI. requireLicense is intentionally skipped — there's nothing
|
|
388
424
|
// to gate. All hooks above already ran, so hook order is preserved, and
|
|
389
|
-
// AutoExternalSync
|
|
390
|
-
// contacted
|
|
425
|
+
// AutoExternalSync still obeys `syncEnabled` (the broker must never be
|
|
426
|
+
// contacted from a shipped build that didn't ask for it).
|
|
391
427
|
if (headless) {
|
|
392
428
|
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
393
|
-
children: [ReduxAutoInstrument && /*#__PURE__*/_jsx(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/_jsx(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/_jsx(RouteTracker, {}),
|
|
394
|
-
options:
|
|
429
|
+
children: [ReduxAutoInstrument && /*#__PURE__*/_jsx(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/_jsx(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/_jsx(RouteTracker, {}), syncEnabled && /*#__PURE__*/_jsx(AutoExternalSync, {
|
|
430
|
+
options: externalSyncOptions(externalSync),
|
|
395
431
|
requiredEnvVars: requiredEnvVars,
|
|
396
432
|
isPro: isLicensed,
|
|
433
|
+
tier: tier,
|
|
397
434
|
licenseKey: licenseKey,
|
|
398
435
|
customApps: syncApps
|
|
399
436
|
})]
|
|
@@ -431,10 +468,11 @@ export const FloatingDevTools = ({
|
|
|
431
468
|
environment: resolvedEnvironment
|
|
432
469
|
}), /*#__PURE__*/_jsx(AppOverlay, {})]
|
|
433
470
|
})
|
|
434
|
-
}), children, DebugBordersOverlay && /*#__PURE__*/_jsx(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/_jsx(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/_jsx(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/_jsx(PerfMonitorOverlay, {}), JsTopOverlay && /*#__PURE__*/_jsx(JsTopOverlay, {}), ImpersonateOverlay && /*#__PURE__*/_jsx(ImpersonateOverlay, {}), ReduxAutoInstrument && /*#__PURE__*/_jsx(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/_jsx(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/_jsx(RouteTracker, {}),
|
|
435
|
-
options:
|
|
471
|
+
}), children, DebugBordersOverlay && /*#__PURE__*/_jsx(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/_jsx(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/_jsx(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/_jsx(PerfMonitorOverlay, {}), JsTopOverlay && /*#__PURE__*/_jsx(JsTopOverlay, {}), ImpersonateOverlay && /*#__PURE__*/_jsx(ImpersonateOverlay, {}), ReduxAutoInstrument && /*#__PURE__*/_jsx(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/_jsx(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/_jsx(RouteTracker, {}), syncEnabled && /*#__PURE__*/_jsx(AutoExternalSync, {
|
|
472
|
+
options: externalSyncOptions(externalSync),
|
|
436
473
|
requiredEnvVars: requiredEnvVars,
|
|
437
474
|
isPro: isLicensed,
|
|
475
|
+
tier: tier,
|
|
438
476
|
licenseKey: licenseKey,
|
|
439
477
|
customApps: syncApps
|
|
440
478
|
})]
|
|
@@ -22,6 +22,7 @@ const FALLBACK_CONTEXT = {
|
|
|
22
22
|
isSearchActive: false,
|
|
23
23
|
clearSearch: noop,
|
|
24
24
|
close: noop,
|
|
25
|
+
minimize: noop,
|
|
25
26
|
showToast: noop,
|
|
26
27
|
setHeaderOverride: noop
|
|
27
28
|
};
|
|
@@ -152,10 +153,13 @@ export function ToolModalShell({
|
|
|
152
153
|
isSearchActive,
|
|
153
154
|
clearSearch,
|
|
154
155
|
close: onClose,
|
|
156
|
+
// Bare call: JsModal persists its own dimensions, so restoring brings
|
|
157
|
+
// the tool back the size the user last left it.
|
|
158
|
+
minimize: () => onMinimize(),
|
|
155
159
|
showToast,
|
|
156
160
|
setHeaderOverride,
|
|
157
161
|
instanceId: app.instanceId
|
|
158
|
-
}), [search, isSearchActive, clearSearch, onClose, showToast, app.instanceId]);
|
|
162
|
+
}), [search, isSearchActive, clearSearch, onClose, onMinimize, showToast, app.instanceId]);
|
|
159
163
|
const title = headerOverride?.title ?? config.title ?? app.title ?? app.id;
|
|
160
164
|
const subtitle = headerOverride?.subtitle ?? config.subtitle;
|
|
161
165
|
const onBack = headerOverride?.onBack;
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Zero-config device→desktop sync for FloatingDevTools.
|
|
5
5
|
*
|
|
6
6
|
* When @buoy-gg/external-sync is installed, FloatingDevTools renders
|
|
7
|
-
* <AutoExternalSync /> in dev builds
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* <AutoExternalSync /> in dev builds (and in release builds that opted in via
|
|
8
|
+
* externalSync.enableInRelease — see externalSyncGate.ts): it connects to the
|
|
9
|
+
* Buoy Desktop broker (port 42831 on the Metro host, so physical devices reach
|
|
10
|
+
* the dev machine automatically — a background retry when the desktop app
|
|
11
|
+
* isn't running) and registers a sync adapter for every installed tool
|
|
12
|
+
* package, discovered with the same guarded-require pattern as
|
|
13
|
+
* autoDiscoverPresets. No <DevToolsSync> wiring needed in the app.
|
|
13
14
|
*
|
|
14
15
|
* Identity defaults come from expo-constants when available (app name →
|
|
15
16
|
* "MyApp (ios)" / "myapp-ios"), overridable via the `externalSync` prop.
|
|
@@ -175,6 +176,7 @@ export function AutoExternalSync({
|
|
|
175
176
|
options,
|
|
176
177
|
requiredEnvVars,
|
|
177
178
|
isPro,
|
|
179
|
+
tier,
|
|
178
180
|
licenseKey,
|
|
179
181
|
customApps
|
|
180
182
|
}) {
|
|
@@ -208,10 +210,12 @@ export function AutoExternalSync({
|
|
|
208
210
|
// record and rebroadcasts, so the desktop dashboard can auto-adopt the key.
|
|
209
211
|
const licenseRef = useRef({
|
|
210
212
|
isPro,
|
|
213
|
+
tier,
|
|
211
214
|
licenseKey
|
|
212
215
|
});
|
|
213
216
|
licenseRef.current = {
|
|
214
217
|
isPro,
|
|
218
|
+
tier,
|
|
215
219
|
licenseKey
|
|
216
220
|
};
|
|
217
221
|
useEffect(() => {
|
|
@@ -219,6 +223,8 @@ export function AutoExternalSync({
|
|
|
219
223
|
const emitLicense = () => {
|
|
220
224
|
socket.emit("device-license", {
|
|
221
225
|
isPro: licenseRef.current.isPro ? "1" : "0",
|
|
226
|
+
// Brokers older than the tier split ignore this; newer ones prefer it.
|
|
227
|
+
tier: licenseRef.current.tier ?? (licenseRef.current.isPro ? "pro" : "anonymous"),
|
|
222
228
|
licenseKey: licenseRef.current.licenseKey ?? ""
|
|
223
229
|
});
|
|
224
230
|
};
|
|
@@ -227,7 +233,7 @@ export function AutoExternalSync({
|
|
|
227
233
|
return () => {
|
|
228
234
|
socket.off("connect", emitLicense);
|
|
229
235
|
};
|
|
230
|
-
}, [socket, isPro, licenseKey]);
|
|
236
|
+
}, [socket, isPro, tier, licenseKey]);
|
|
231
237
|
const tools = useMemo(() => {
|
|
232
238
|
// Core's own adapter — device-level capabilities (JS-bundle reload) that
|
|
233
239
|
// don't belong to any tool package. Registered unconditionally so remote
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether FloatingDevTools mounts <AutoExternalSync /> — i.e. whether
|
|
5
|
+
* this build is allowed to dial the Buoy Desktop broker.
|
|
6
|
+
*
|
|
7
|
+
* Kept as a pure function in its own module (no react-native / no guarded
|
|
8
|
+
* requires) so the rule is unit-testable: the gate used to be an inline
|
|
9
|
+
* `isDevelopment && ...` in JSX, which silently made release builds unable to
|
|
10
|
+
* reach the desktop dashboard or MCP with no way to opt in from app code.
|
|
11
|
+
*
|
|
12
|
+
* The rule has two halves that must BOTH hold in a release build, because a
|
|
13
|
+
* shipped app must never contact a broker on an end user's device:
|
|
14
|
+
* 1. a real Pro license (never the Weekend Pass — see useProAccess), and
|
|
15
|
+
* 2. an EXPLICIT opt-in from app code (`externalSync.enableInRelease`).
|
|
16
|
+
* Dev builds keep the zero-config behavior: sync is on unless turned off.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The options object from the prop, or undefined when it's a boolean. */
|
|
20
|
+
export function externalSyncOptions(externalSync) {
|
|
21
|
+
return typeof externalSync === "object" && externalSync !== null ? externalSync : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function shouldMountExternalSync({
|
|
24
|
+
isDevelopment,
|
|
25
|
+
isLicensed,
|
|
26
|
+
externalSync,
|
|
27
|
+
isAvailable
|
|
28
|
+
}) {
|
|
29
|
+
// Turned off outright, or the package isn't installed — nothing to mount.
|
|
30
|
+
if (externalSync === false) return false;
|
|
31
|
+
if (!isAvailable) return false;
|
|
32
|
+
|
|
33
|
+
// Dev builds: zero-config, on by default (historical behavior).
|
|
34
|
+
if (isDevelopment) return true;
|
|
35
|
+
|
|
36
|
+
// Release builds: opt-in only, and Pro only. `enableInRelease` has to be set
|
|
37
|
+
// by the app itself, so an app that just ships FloatingDevTools with a Pro
|
|
38
|
+
// key still never opens a socket on a customer's phone.
|
|
39
|
+
//
|
|
40
|
+
// The `isLicensed` half is belt-and-braces: FloatingDevTools already renders
|
|
41
|
+
// nothing at all for an unlicensed release build. Keep it anyway so this rule
|
|
42
|
+
// stands on its own and can't be broken by a change to that earlier return.
|
|
43
|
+
return isLicensed && externalSyncOptions(externalSync)?.enableInRelease === true;
|
|
44
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { resolveReloadStrategy, scheduleReloadApp } from "@buoy-gg/shared-ui";
|
|
3
|
+
import { getDevServerOrigin, resolveReloadStrategy, scheduleReloadApp } from "@buoy-gg/shared-ui";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Sync adapter for the "app" pseudo-tool — device-level capabilities that
|
|
@@ -24,12 +24,24 @@ import { resolveReloadStrategy, scheduleReloadApp } from "@buoy-gg/shared-ui";
|
|
|
24
24
|
* and reporting it too early.
|
|
25
25
|
*/
|
|
26
26
|
const REALM_STARTED_AT = Date.now();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Where this bundle came from, resolved once at module load.
|
|
30
|
+
*
|
|
31
|
+
* Reported so a remote driver can recover the app when the app itself can no
|
|
32
|
+
* longer be asked to: after a fatal render error the React tree is gone, every
|
|
33
|
+
* tool action goes unanswered, and `reloadApp` below is unreachable. The dev
|
|
34
|
+
* server's reload path runs natively and still works — but only for a driver
|
|
35
|
+
* that learned this origin while the app was healthy.
|
|
36
|
+
*/
|
|
37
|
+
const DEV_SERVER_ORIGIN = getDevServerOrigin();
|
|
27
38
|
export const appSyncAdapter = {
|
|
28
|
-
version:
|
|
39
|
+
version: 2,
|
|
29
40
|
getSnapshot: () => {
|
|
30
41
|
const strategy = resolveReloadStrategy("auto");
|
|
31
42
|
return {
|
|
32
43
|
startedAt: REALM_STARTED_AT,
|
|
44
|
+
devServerOrigin: DEV_SERVER_ORIGIN,
|
|
33
45
|
reload: {
|
|
34
46
|
available: strategy !== null,
|
|
35
47
|
strategy
|
|
@@ -47,7 +59,8 @@ export const appSyncAdapter = {
|
|
|
47
59
|
*/
|
|
48
60
|
ping: () => ({
|
|
49
61
|
ok: true,
|
|
50
|
-
startedAt: REALM_STARTED_AT
|
|
62
|
+
startedAt: REALM_STARTED_AT,
|
|
63
|
+
devServerOrigin: DEV_SERVER_ORIGIN
|
|
51
64
|
}),
|
|
52
65
|
/**
|
|
53
66
|
* Reload the app's JS bundle: RN's `DevSettings.reload()` in dev builds,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anonymous install ping.
|
|
5
|
+
*
|
|
6
|
+
* Answers the one question licensing data cannot: how many people run Buoy at
|
|
7
|
+
* all. Keygen sees every free and paid key, but by definition it cannot see an
|
|
8
|
+
* install that never claimed one — so without this, "what fraction of installs
|
|
9
|
+
* have an account" has no denominator.
|
|
10
|
+
*
|
|
11
|
+
* Constraints this is built to honor:
|
|
12
|
+
*
|
|
13
|
+
* - **Development only.** Never fires from a shipped app. Buoy is a developer
|
|
14
|
+
* tool; pinging from a release build would phone home from an end user's
|
|
15
|
+
* device — someone who never chose to use Buoy — which is not a trade we are
|
|
16
|
+
* willing to make for a metric.
|
|
17
|
+
* - **No PII, no fingerprint.** The install id is a random UUID minted here.
|
|
18
|
+
* It is not derived from hardware, account, project, or app identity, so it
|
|
19
|
+
* cannot be correlated to a person or joined against anything else we hold.
|
|
20
|
+
* - **No tool usage.** What you open and inspect stays on your machine, which
|
|
21
|
+
* is the whole security story for enterprise adoption. Only the four fields
|
|
22
|
+
* below ever leave.
|
|
23
|
+
* - **Silent and harmless.** Fire-and-forget, throttled to once a day, and
|
|
24
|
+
* every failure path is swallowed. It can never block, slow, or break an app.
|
|
25
|
+
*
|
|
26
|
+
* Opt out with `Buoy.init({ telemetry: false })`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { Platform } from "react-native";
|
|
30
|
+
import { persistentStorage } from "@buoy-gg/shared-ui";
|
|
31
|
+
import { SDK_VERSION } from "../version.js";
|
|
32
|
+
|
|
33
|
+
/** Same `@react_buoy` prefix as every other Buoy key, so it is inspectable. */
|
|
34
|
+
const STORAGE_KEY = "@react_buoy_install";
|
|
35
|
+
const ENDPOINT = "https://buoy.gg/api/t";
|
|
36
|
+
|
|
37
|
+
/** One ping per install per day is enough to count installs and daily actives. */
|
|
38
|
+
const PING_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
39
|
+
/** RFC4122 v4, from Math.random — this is a counter, not a credential. */
|
|
40
|
+
function uuid() {
|
|
41
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
42
|
+
const r = Math.random() * 16 | 0;
|
|
43
|
+
const v = c === "x" ? r : r & 0x3 | 0x8;
|
|
44
|
+
return v.toString(16);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function readState() {
|
|
48
|
+
try {
|
|
49
|
+
const raw = await persistentStorage.getItem(STORAGE_KEY);
|
|
50
|
+
if (!raw) return null;
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (typeof parsed?.installId !== "string") return null;
|
|
53
|
+
return {
|
|
54
|
+
installId: parsed.installId,
|
|
55
|
+
lastPingAt: typeof parsed.lastPingAt === "number" ? parsed.lastPingAt : 0
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Told once, on the very first ping of a new install — not on every launch.
|
|
64
|
+
*
|
|
65
|
+
* A dev tool that phones home silently deserves the reputation it would get.
|
|
66
|
+
* One line, the first time, saying exactly what leaves and how to stop it.
|
|
67
|
+
*/
|
|
68
|
+
function announceOnce() {
|
|
69
|
+
console.log("[Buoy] Anonymous usage ping enabled: a random install id, the Buoy " + "version, your platform, and your license tier — no project, app, or " + "tool data, and only in development. Opt out with " + "Buoy.init({ telemetry: false }). Details: https://buoy.gg/docs/telemetry");
|
|
70
|
+
}
|
|
71
|
+
let sentThisSession = false;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Send the ping if it is due. Safe to call on every mount: it is idempotent
|
|
75
|
+
* within a session, throttled to once a day across sessions, and resolves
|
|
76
|
+
* without throwing no matter what fails.
|
|
77
|
+
*/
|
|
78
|
+
export async function sendInstallPing(options) {
|
|
79
|
+
// Never from a shipped app — see the header. This is the single most
|
|
80
|
+
// important line in the file.
|
|
81
|
+
const isDevelopment = typeof __DEV__ !== "undefined" && __DEV__;
|
|
82
|
+
if (!isDevelopment) return;
|
|
83
|
+
if (!options.enabled) return;
|
|
84
|
+
if (sentThisSession) return;
|
|
85
|
+
sentThisSession = true;
|
|
86
|
+
try {
|
|
87
|
+
const existing = await readState();
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (existing && now - existing.lastPingAt < PING_INTERVAL_MS) return;
|
|
90
|
+
const installId = existing?.installId ?? uuid();
|
|
91
|
+
if (!existing) announceOnce();
|
|
92
|
+
|
|
93
|
+
// Recorded BEFORE the request, so a server that is down or unreachable
|
|
94
|
+
// can't turn into a ping every single mount.
|
|
95
|
+
await persistentStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
96
|
+
installId,
|
|
97
|
+
lastPingAt: now
|
|
98
|
+
}));
|
|
99
|
+
await fetch(ENDPOINT, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json"
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
installId,
|
|
106
|
+
version: SDK_VERSION,
|
|
107
|
+
platform: Platform.OS,
|
|
108
|
+
tier: options.tier
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
// Offline, firewalled, endpoint down — all fine. A metric is never worth
|
|
113
|
+
// a warning in someone else's console.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Test seam: forget that a ping already went out this session. */
|
|
118
|
+
export function __resetInstallPingForTests() {
|
|
119
|
+
sentThisSession = false;
|
|
120
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This package's published version.
|
|
5
|
+
*
|
|
6
|
+
* Hardcoded rather than read from package.json: `require("../package.json")`
|
|
7
|
+
* resolves to a different depth from `src/` than from the built `lib/`, and
|
|
8
|
+
* react-native-builder-bob (babel) has no build-time `define` the way tsup does
|
|
9
|
+
* for @buoy-gg/mcp.
|
|
10
|
+
*
|
|
11
|
+
* Drift is prevented by a test that fails when this disagrees with
|
|
12
|
+
* package.json, so a version bump that forgets this file cannot ship.
|
|
13
|
+
*/
|
|
14
|
+
export const SDK_VERSION = "7.0.22";
|