@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.
Files changed (51) hide show
  1. package/bin/__tests__/buoyCli.test.ts +131 -0
  2. package/bin/buoy.js +299 -0
  3. package/lib/commonjs/Buoy.js +14 -0
  4. package/lib/commonjs/floatingMenu/FloatingDevTools.js +48 -10
  5. package/lib/commonjs/floatingMenu/ToolHost.js +5 -1
  6. package/lib/commonjs/floatingMenu/autoExternalSync.js +13 -7
  7. package/lib/commonjs/floatingMenu/externalSyncGate.js +49 -0
  8. package/lib/commonjs/floatingMenu/sync/appSyncAdapter.js +15 -2
  9. package/lib/commonjs/telemetry/installPing.js +124 -0
  10. package/lib/commonjs/version.js +18 -0
  11. package/lib/module/Buoy.js +13 -0
  12. package/lib/module/floatingMenu/FloatingDevTools.js +48 -10
  13. package/lib/module/floatingMenu/ToolHost.js +5 -1
  14. package/lib/module/floatingMenu/autoExternalSync.js +13 -7
  15. package/lib/module/floatingMenu/externalSyncGate.js +44 -0
  16. package/lib/module/floatingMenu/sync/appSyncAdapter.js +16 -3
  17. package/lib/module/telemetry/installPing.js +120 -0
  18. package/lib/module/version.js +14 -0
  19. package/lib/typescript/commonjs/Buoy.d.ts +14 -0
  20. package/lib/typescript/commonjs/Buoy.d.ts.map +1 -1
  21. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +15 -0
  22. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts +7 -1
  24. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +28 -2
  26. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/floatingMenu/externalSyncGate.d.ts +30 -0
  28. package/lib/typescript/commonjs/floatingMenu/externalSyncGate.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts +2 -0
  30. package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/telemetry/installPing.d.ts +37 -0
  32. package/lib/typescript/commonjs/telemetry/installPing.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/version.d.ts +13 -0
  34. package/lib/typescript/commonjs/version.d.ts.map +1 -0
  35. package/lib/typescript/module/Buoy.d.ts +14 -0
  36. package/lib/typescript/module/Buoy.d.ts.map +1 -1
  37. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +15 -0
  38. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  39. package/lib/typescript/module/floatingMenu/ToolHost.d.ts +7 -1
  40. package/lib/typescript/module/floatingMenu/ToolHost.d.ts.map +1 -1
  41. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +28 -2
  42. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  43. package/lib/typescript/module/floatingMenu/externalSyncGate.d.ts +30 -0
  44. package/lib/typescript/module/floatingMenu/externalSyncGate.d.ts.map +1 -0
  45. package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts +2 -0
  46. package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
  47. package/lib/typescript/module/telemetry/installPing.d.ts +37 -0
  48. package/lib/typescript/module/telemetry/installPing.d.ts.map +1 -0
  49. package/lib/typescript/module/version.d.ts +13 -0
  50. package/lib/typescript/module/version.d.ts.map +1 -0
  51. package/package.json +9 -5
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The CLI writes into someone else's repo, so the failure modes that matter are
3
+ * "silently wrote the wrong variable name" and "clobbered a file I cared about".
4
+ */
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { createRequire } from "node:module";
10
+
11
+ const require_ = createRequire(import.meta.url);
12
+ const cli = require_("../buoy.js") as {
13
+ detectEnvVarName: (root: string) => string;
14
+ writeEnvVar: (root: string, name: string, value: string) => string;
15
+ ensureGitignored: (root: string) => boolean;
16
+ };
17
+
18
+ let root: string;
19
+
20
+ beforeEach(() => {
21
+ root = mkdtempSync(join(tmpdir(), "buoy-cli-"));
22
+ });
23
+ afterEach(() => {
24
+ rmSync(root, { recursive: true, force: true });
25
+ });
26
+
27
+ const write = (rel: string, body: string) => {
28
+ mkdirSync(join(root, rel, ".."), { recursive: true });
29
+ writeFileSync(join(root, rel), body);
30
+ };
31
+ const read = (rel: string) => readFileSync(join(root, rel), "utf8");
32
+
33
+ describe("detectEnvVarName", () => {
34
+ it("uses the EXPO_PUBLIC_ prefix for Expo projects", () => {
35
+ // Expo only inlines EXPO_PUBLIC_* into the bundle. Writing a bare BUOY_KEY
36
+ // there produces a file that looks right and does nothing at runtime.
37
+ write("package.json", JSON.stringify({ dependencies: { expo: "^52.0.0" } }));
38
+ expect(cli.detectEnvVarName(root)).toBe("EXPO_PUBLIC_BUOY_KEY");
39
+ });
40
+
41
+ it("detects Expo from app.config.js even without the dependency listed", () => {
42
+ write("package.json", JSON.stringify({}));
43
+ write("app.config.js", "module.exports = {};");
44
+ expect(cli.detectEnvVarName(root)).toBe("EXPO_PUBLIC_BUOY_KEY");
45
+ });
46
+
47
+ it("detects Expo from an app.json with an expo block", () => {
48
+ write("package.json", JSON.stringify({}));
49
+ write("app.json", JSON.stringify({ expo: { name: "demo" } }));
50
+ expect(cli.detectEnvVarName(root)).toBe("EXPO_PUBLIC_BUOY_KEY");
51
+ });
52
+
53
+ it("falls back to BUOY_KEY for bare React Native", () => {
54
+ write("package.json", JSON.stringify({ dependencies: { "react-native": "0.76.0" } }));
55
+ expect(cli.detectEnvVarName(root)).toBe("BUOY_KEY");
56
+ });
57
+
58
+ it("survives an unreadable or malformed package.json", () => {
59
+ write("package.json", "{ not json");
60
+ expect(cli.detectEnvVarName(root)).toBe("BUOY_KEY");
61
+ });
62
+ });
63
+
64
+ describe("writeEnvVar", () => {
65
+ it("creates .env.local when there isn't one", () => {
66
+ cli.writeEnvVar(root, "BUOY_KEY", "ABC-123");
67
+ expect(read(".env.local")).toBe("BUOY_KEY=ABC-123\n");
68
+ });
69
+
70
+ it("keeps every other variable intact", () => {
71
+ write(".env.local", "API_URL=https://example.test\nDEBUG=1\n");
72
+ cli.writeEnvVar(root, "BUOY_KEY", "ABC-123");
73
+ const out = read(".env.local");
74
+ expect(out).toContain("API_URL=https://example.test");
75
+ expect(out).toContain("DEBUG=1");
76
+ expect(out).toContain("BUOY_KEY=ABC-123");
77
+ });
78
+
79
+ it("replaces an existing key instead of appending a duplicate", () => {
80
+ // Running `buoy login` twice must not leave two BUOY_KEY lines, where which
81
+ // one wins depends on the dotenv parser.
82
+ write(".env.local", "BUOY_KEY=OLD-KEY\nOTHER=1\n");
83
+ cli.writeEnvVar(root, "BUOY_KEY", "NEW-KEY");
84
+ const out = read(".env.local");
85
+ expect(out).toContain("BUOY_KEY=NEW-KEY");
86
+ expect(out).not.toContain("OLD-KEY");
87
+ expect(out.match(/BUOY_KEY=/g)).toHaveLength(1);
88
+ expect(out).toContain("OTHER=1");
89
+ });
90
+
91
+ it("does not glue the new line onto a file with no trailing newline", () => {
92
+ write(".env.local", "OTHER=1");
93
+ cli.writeEnvVar(root, "BUOY_KEY", "ABC");
94
+ expect(read(".env.local")).toBe("OTHER=1\nBUOY_KEY=ABC\n");
95
+ });
96
+
97
+ it("does not confuse a similarly named variable", () => {
98
+ write(".env.local", "EXPO_PUBLIC_BUOY_KEY=EXPO-ONE\n");
99
+ cli.writeEnvVar(root, "BUOY_KEY", "BARE-ONE");
100
+ const out = read(".env.local");
101
+ expect(out).toContain("EXPO_PUBLIC_BUOY_KEY=EXPO-ONE");
102
+ expect(out).toContain("\nBUOY_KEY=BARE-ONE");
103
+ });
104
+ });
105
+
106
+ describe("ensureGitignored", () => {
107
+ it("adds .env.local when it isn't ignored", () => {
108
+ write(".gitignore", "node_modules\n");
109
+ expect(cli.ensureGitignored(root)).toBe(true);
110
+ expect(read(".gitignore")).toContain(".env.local");
111
+ });
112
+
113
+ it("leaves an already-ignored file alone", () => {
114
+ write(".gitignore", "node_modules\n.env.local\n");
115
+ expect(cli.ensureGitignored(root)).toBe(false);
116
+ expect(read(".gitignore").match(/\.env\.local/g)).toHaveLength(1);
117
+ });
118
+
119
+ it("recognizes the .env* and *.local wildcards", () => {
120
+ write(".gitignore", ".env*\n");
121
+ expect(cli.ensureGitignored(root)).toBe(false);
122
+
123
+ write(".gitignore", "*.local\n");
124
+ expect(cli.ensureGitignored(root)).toBe(false);
125
+ });
126
+
127
+ it("does nothing when the project has no .gitignore", () => {
128
+ // Creating one would be presumptuous in a repo that deliberately has none.
129
+ expect(cli.ensureGitignored(root)).toBe(false);
130
+ });
131
+ });
package/bin/buoy.js ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `buoy` CLI.
4
+ *
5
+ * Ships as a bin on @buoy-gg/core rather than its own package so that
6
+ * `npx buoy login` resolves from the project's own node_modules — anyone who
7
+ * already installed Buoy has it, with nothing extra to install and no npm
8
+ * package name to own.
9
+ *
10
+ * Plain CommonJS with ZERO dependencies, on purpose: it runs via npx inside
11
+ * someone else's project, so every dependency would be an install-time cost and
12
+ * a supply-chain surface for a tool whose entire job takes ten seconds.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const http = require('node:http');
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+ const crypto = require('node:crypto');
21
+ const { spawn } = require('node:child_process');
22
+
23
+ const SITE = process.env.BUOY_SITE_URL || 'https://buoy.gg';
24
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
25
+
26
+ const ESC = '\x1b[';
27
+ const color = (code) => (s) => `${ESC}${code}m${s}${ESC}0m`;
28
+ const cyan = color(36);
29
+ const green = color(32);
30
+ const red = color(31);
31
+ const dim = color(2);
32
+ const bold = color(1);
33
+
34
+ function log(msg) {
35
+ process.stdout.write(`${msg}\n`);
36
+ }
37
+
38
+ function fail(msg) {
39
+ process.stderr.write(`${red('x')} ${msg}\n`);
40
+ process.exit(1);
41
+ }
42
+
43
+ /**
44
+ * Open a URL in the user's default browser.
45
+ *
46
+ * Failure is not fatal: headless boxes, SSH sessions and locked-down machines
47
+ * all land here, and the URL is printed regardless so it can be opened by hand.
48
+ */
49
+ function openBrowser(url) {
50
+ const cmd =
51
+ process.platform === 'darwin'
52
+ ? 'open'
53
+ : process.platform === 'win32'
54
+ ? 'cmd'
55
+ : 'xdg-open';
56
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
57
+ try {
58
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
59
+ } catch {
60
+ /* the URL is printed either way */
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Which env var name this project can actually read.
66
+ *
67
+ * Expo only inlines `EXPO_PUBLIC_`-prefixed vars into the bundle, so writing a
68
+ * bare `BUOY_KEY` there produces a file that looks correct and silently does
69
+ * nothing — the most likely way this whole flow could waste someone's evening.
70
+ */
71
+ function detectEnvVarName(root) {
72
+ try {
73
+ const pkgPath = path.join(root, 'package.json');
74
+ if (fs.existsSync(pkgPath)) {
75
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
76
+ const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
77
+ if (deps.expo) return 'EXPO_PUBLIC_BUOY_KEY';
78
+ }
79
+ if (
80
+ fs.existsSync(path.join(root, 'app.config.js')) ||
81
+ fs.existsSync(path.join(root, 'app.config.ts'))
82
+ ) {
83
+ return 'EXPO_PUBLIC_BUOY_KEY';
84
+ }
85
+ const appJson = path.join(root, 'app.json');
86
+ if (fs.existsSync(appJson)) {
87
+ const parsed = JSON.parse(fs.readFileSync(appJson, 'utf8'));
88
+ if (parsed && parsed.expo) return 'EXPO_PUBLIC_BUOY_KEY';
89
+ }
90
+ } catch {
91
+ /* fall through to the generic name */
92
+ }
93
+ return 'BUOY_KEY';
94
+ }
95
+
96
+ /** Upsert `NAME=value` in .env.local, preserving everything else in the file. */
97
+ function writeEnvVar(root, name, value) {
98
+ const file = path.join(root, '.env.local');
99
+ let contents = '';
100
+ try {
101
+ contents = fs.readFileSync(file, 'utf8');
102
+ } catch {
103
+ /* new file */
104
+ }
105
+
106
+ const line = `${name}=${value}`;
107
+ const pattern = new RegExp('^' + name + '=.*$', 'm');
108
+
109
+ if (pattern.test(contents)) {
110
+ contents = contents.replace(pattern, line);
111
+ } else {
112
+ if (contents && !contents.endsWith('\n')) contents += '\n';
113
+ contents += `${line}\n`;
114
+ }
115
+
116
+ fs.writeFileSync(file, contents, { mode: 0o600 });
117
+ return file;
118
+ }
119
+
120
+ /**
121
+ * Make sure .env.local is ignored.
122
+ *
123
+ * The whole point of a per-developer key is that it identifies one person; a
124
+ * key committed to a shared repo turns the count back into "repos", which is
125
+ * exactly what this flow exists to avoid.
126
+ */
127
+ function ensureGitignored(root) {
128
+ const file = path.join(root, '.gitignore');
129
+ let contents = '';
130
+ try {
131
+ contents = fs.readFileSync(file, 'utf8');
132
+ } catch {
133
+ return false;
134
+ }
135
+ if (/^\.env\.local$|^\.env\*|^\*\.local$/m.test(contents)) return false;
136
+
137
+ if (contents && !contents.endsWith('\n')) contents += '\n';
138
+ contents += '\n# Buoy license key (per developer - do not commit)\n.env.local\n';
139
+ fs.writeFileSync(file, contents);
140
+ return true;
141
+ }
142
+
143
+ /**
144
+ * Loopback auth.
145
+ *
146
+ * The browser does the signing in (it already holds the Clerk session), then
147
+ * hands the key back to a short-lived server on 127.0.0.1. Chosen over a
148
+ * device-code flow because it needs no server-side pending-code state, and over
149
+ * redirecting with the key in the URL because that would write a credential
150
+ * into browser history.
151
+ */
152
+ function awaitKeyFromBrowser() {
153
+ return new Promise((resolve, reject) => {
154
+ const state = crypto.randomBytes(16).toString('hex');
155
+
156
+ const server = http.createServer((req, res) => {
157
+ // The page runs on buoy.gg and posts here cross-origin.
158
+ if (req.headers.origin === SITE) {
159
+ res.setHeader('Access-Control-Allow-Origin', SITE);
160
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
161
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
162
+ }
163
+
164
+ if (req.method === 'OPTIONS') {
165
+ res.writeHead(204).end();
166
+ return;
167
+ }
168
+ if (req.method !== 'POST') {
169
+ res.writeHead(405).end();
170
+ return;
171
+ }
172
+
173
+ let body = '';
174
+ req.on('data', (chunk) => {
175
+ body += chunk;
176
+ // A local server that accepts unbounded input is a local server that
177
+ // can be made to eat all your memory.
178
+ if (body.length > 4096) req.destroy();
179
+ });
180
+ req.on('end', () => {
181
+ let parsed = {};
182
+ try {
183
+ parsed = JSON.parse(body);
184
+ } catch {
185
+ /* handled by the check below */
186
+ }
187
+
188
+ // Any page in any tab can POST to localhost. The nonce is what proves
189
+ // this response belongs to the login WE just started.
190
+ if (parsed.state !== state || typeof parsed.key !== 'string' || !parsed.key) {
191
+ res.writeHead(400).end();
192
+ return;
193
+ }
194
+
195
+ res.writeHead(200, { 'Content-Type': 'application/json' });
196
+ res.end('{"ok":true}');
197
+ server.close();
198
+ resolve({
199
+ key: parsed.key.trim(),
200
+ tier: parsed.tier || 'free',
201
+ email: parsed.email || '',
202
+ });
203
+ });
204
+ });
205
+
206
+ server.on('error', reject);
207
+
208
+ // Port 0 = let the OS pick. Bound to loopback only, so nothing on the
209
+ // network can reach it.
210
+ server.listen(0, '127.0.0.1', () => {
211
+ const { port } = server.address();
212
+ const url = `${SITE}/cli/login?port=${port}&state=${state}`;
213
+
214
+ log('');
215
+ log(` Opening ${cyan(url)}`);
216
+ log(dim(' If your browser did not open, paste that URL into it.'));
217
+ log('');
218
+ log(dim(' Waiting for you to finish signing in...'));
219
+
220
+ openBrowser(url);
221
+ });
222
+
223
+ setTimeout(() => {
224
+ server.close();
225
+ reject(new Error('Timed out waiting for the browser (5 minutes).'));
226
+ }, LOGIN_TIMEOUT_MS).unref();
227
+ });
228
+ }
229
+
230
+ async function login() {
231
+ const root = process.cwd();
232
+
233
+ let result;
234
+ try {
235
+ result = await awaitKeyFromBrowser();
236
+ } catch (err) {
237
+ fail(`${err.message}\n Nothing was written.`);
238
+ return;
239
+ }
240
+
241
+ const varName = detectEnvVarName(root);
242
+ const file = writeEnvVar(root, varName, result.key);
243
+ const addedIgnore = ensureGitignored(root);
244
+ const relative = path.relative(root, file) || '.env.local';
245
+
246
+ log('');
247
+ log(
248
+ `${green('OK')} Signed in${result.email ? ' as ' + bold(result.email) : ''} - ${bold(result.tier)} tier`
249
+ );
250
+ log(` Wrote ${cyan(varName)} to ${cyan(relative)}`);
251
+ if (addedIgnore) log(dim(' Added .env.local to .gitignore'));
252
+ log('');
253
+ log(' Use it in your app:');
254
+ log('');
255
+ log(cyan(` Buoy.init({ licenseKey: process.env.${varName} });`));
256
+ log('');
257
+ log(dim(' Restart your bundler for the new env var to take effect.'));
258
+ log('');
259
+ }
260
+
261
+ function help() {
262
+ log(`
263
+ ${bold('buoy')} - React Buoy CLI
264
+
265
+ ${bold('Usage')}
266
+ npx buoy <command>
267
+
268
+ ${bold('Commands')}
269
+ login Sign in and write your license key to .env.local
270
+ help Show this message
271
+
272
+ ${bold('Notes')}
273
+ A free key takes about thirty seconds and needs no card. Paid keys use the
274
+ same command - it writes whichever key your account already holds.
275
+
276
+ Docs: ${SITE}/docs/installation
277
+ `);
278
+ }
279
+
280
+ // Only dispatch when run as a program. Without this guard, importing the file
281
+ // in a test would parse the test runner's own argv and exit the process.
282
+ if (require.main === module) {
283
+ const command = process.argv[2];
284
+
285
+ if (command === 'login') {
286
+ login();
287
+ } else if (!command || command === 'help' || command === '--help' || command === '-h') {
288
+ help();
289
+ } else {
290
+ fail(`Unknown command: ${command}\n Run \`npx buoy help\` to see what is available.`);
291
+ }
292
+ }
293
+
294
+ module.exports = {
295
+ detectEnvVarName,
296
+ writeEnvVar,
297
+ ensureGitignored,
298
+ awaitKeyFromBrowser,
299
+ };
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = exports.Buoy = void 0;
7
+ exports.isTelemetryEnabled = isTelemetryEnabled;
7
8
  var _license = require("@buoy-gg/license");
8
9
  /**
9
10
  * Buoy SDK - Main entry point for React Buoy configuration
@@ -20,6 +21,17 @@ var _license = require("@buoy-gg/license");
20
21
  * ```
21
22
  */
22
23
 
24
+ /**
25
+ * Telemetry stays on unless an app explicitly opts out. Read at ping time
26
+ * rather than captured, so `Buoy.init()` still wins if it runs after mount.
27
+ */
28
+ let telemetryEnabled = true;
29
+
30
+ /** Whether the anonymous install ping is allowed to fire. */
31
+ function isTelemetryEnabled() {
32
+ return telemetryEnabled;
33
+ }
34
+
23
35
  /**
24
36
  * Buoy SDK namespace
25
37
  *
@@ -48,6 +60,8 @@ const Buoy = exports.Buoy = {
48
60
  * ```
49
61
  */
50
62
  init(config) {
63
+ if (config.telemetry === false) telemetryEnabled = false;
64
+
51
65
  // If no license key or empty string, clear any cached license
52
66
  if (!config.licenseKey || config.licenseKey.trim() === "") {
53
67
  _license.LicenseManager.initialize().then(() => _license.LicenseManager.clearLicense()).catch(() => {});
@@ -9,6 +9,8 @@ var _reactNative = require("react-native");
9
9
  var _AppHost = require("./AppHost");
10
10
  var _FloatingMenu = require("./FloatingMenu.js");
11
11
  var _autoDiscoverPresets = require("./autoDiscoverPresets.js");
12
+ var _installPing = require("../telemetry/installPing.js");
13
+ var _Buoy = require("../Buoy.js");
12
14
  var _AppIcon = require("./AppIcon.js");
13
15
  var _DevToolsVisibilityContext = require("./DevToolsVisibilityContext.js");
14
16
  var _optionalRequire = require("./optionalRequire.js");
@@ -18,6 +20,7 @@ var _defaultConfig = require("./defaultConfig.js");
18
20
  var _DefaultConfigContext = require("./DefaultConfigContext.js");
19
21
  var _license = require("@buoy-gg/license");
20
22
  var _autoExternalSync = require("./autoExternalSync.js");
23
+ var _externalSyncGate = require("./externalSyncGate.js");
21
24
  var _jsxRuntime = require("react/jsx-runtime");
22
25
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
23
26
  /**
@@ -133,8 +136,20 @@ const FloatingDevTools = ({
133
136
  // Get full license state for requireLicense gating
134
137
  const {
135
138
  licenseKey,
136
- isInitialized
139
+ isInitialized,
140
+ tier
137
141
  } = (0, _license.useLicense)();
142
+
143
+ // Anonymous install ping — dev-only, once a day, no project or tool data.
144
+ // Deliberately waits for the license to resolve so the tier it reports is
145
+ // the real one rather than the "anonymous" the manager starts at.
146
+ (0, _react.useEffect)(() => {
147
+ if (!isInitialized) return;
148
+ void (0, _installPing.sendInstallPing)({
149
+ tier,
150
+ enabled: (0, _Buoy.isTelemetryEnabled)()
151
+ });
152
+ }, [isInitialized, tier]);
138
153
  const [showLicenseModal, setShowLicenseModal] = (0, _react.useState)(false);
139
154
 
140
155
  // Initialize license manager on mount, and set/clear license key when prop changes.
@@ -379,25 +394,47 @@ const FloatingDevTools = ({
379
394
  });
380
395
  }, [finalApps]);
381
396
 
382
- // Gate: Free tier only works in development mode
383
- // Pro users can use DevTools everywhere (dev + production)
397
+ // Gate: only a PAID license works outside development.
398
+ //
399
+ // Both untiered surfaces below it — anonymous (no key) and free (a key on the
400
+ // Free policy) — stay dev-only. `isLicensed` is safe to gate on because
401
+ // `isPro` is a strict mirror of `tier === "pro"`, so a free key reports false
402
+ // here even though it validates perfectly well.
403
+ //
404
+ // When Business on-device login lands, this becomes the boundary that a
405
+ // logged-in QA/support seat crosses WITHOUT a key in the build — i.e.
406
+ // `isLicensed || tierSource === "session"`. Nothing else should.
384
407
  const isDevelopment = typeof __DEV__ !== "undefined" && __DEV__;
385
408
  if (!isDevelopment && !isLicensed) {
386
- // Free user in production - don't render DevTools
409
+ // Anonymous or free user in production - don't render DevTools.
410
+ // NOTE for whoever relaxes this for the free tier: desktop sync does NOT
411
+ // ride on this check. It re-tests `isLicensed` itself (externalSyncGate),
412
+ // so opening this door does not open a broker socket. Keep it that way.
387
413
  return null;
388
414
  }
389
415
 
416
+ // Zero-config in dev; in a release bundle the app must opt in explicitly
417
+ // (externalSync.enableInRelease) AND hold a real Pro license — see
418
+ // externalSyncGate.ts for why both halves are required.
419
+ const syncEnabled = (0, _externalSyncGate.shouldMountExternalSync)({
420
+ isDevelopment,
421
+ isLicensed,
422
+ externalSync,
423
+ isAvailable: (0, _autoExternalSync.isExternalSyncAvailable)()
424
+ });
425
+
390
426
  // Headless (sync-only): tool adapters + route tracking + desktop sync, no
391
427
  // on-device UI. requireLicense is intentionally skipped — there's nothing
392
428
  // to gate. All hooks above already ran, so hook order is preserved, and
393
- // AutoExternalSync is additionally dev-gated (the broker must never be
394
- // contacted in production).
429
+ // AutoExternalSync still obeys `syncEnabled` (the broker must never be
430
+ // contacted from a shipped build that didn't ask for it).
395
431
  if (headless) {
396
432
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
397
- children: [ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), isDevelopment && externalSync !== false && (0, _autoExternalSync.isExternalSyncAvailable)() && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
398
- options: typeof externalSync === "object" ? externalSync : undefined,
433
+ children: [ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), syncEnabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
434
+ options: (0, _externalSyncGate.externalSyncOptions)(externalSync),
399
435
  requiredEnvVars: requiredEnvVars,
400
436
  isPro: isLicensed,
437
+ tier: tier,
401
438
  licenseKey: licenseKey,
402
439
  customApps: syncApps
403
440
  })]
@@ -435,10 +472,11 @@ const FloatingDevTools = ({
435
472
  environment: resolvedEnvironment
436
473
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_AppHost.AppOverlay, {})]
437
474
  })
438
- }), children, DebugBordersOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(PerfMonitorOverlay, {}), JsTopOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(JsTopOverlay, {}), ImpersonateOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImpersonateOverlay, {}), ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), isDevelopment && externalSync !== false && (0, _autoExternalSync.isExternalSyncAvailable)() && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
439
- options: typeof externalSync === "object" ? externalSync : undefined,
475
+ }), children, DebugBordersOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(PerfMonitorOverlay, {}), JsTopOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(JsTopOverlay, {}), ImpersonateOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImpersonateOverlay, {}), ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), syncEnabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
476
+ options: (0, _externalSyncGate.externalSyncOptions)(externalSync),
440
477
  requiredEnvVars: requiredEnvVars,
441
478
  isPro: isLicensed,
479
+ tier: tier,
442
480
  licenseKey: licenseKey,
443
481
  customApps: syncApps
444
482
  })]
@@ -30,6 +30,7 @@ const FALLBACK_CONTEXT = {
30
30
  isSearchActive: false,
31
31
  clearSearch: noop,
32
32
  close: noop,
33
+ minimize: noop,
33
34
  showToast: noop,
34
35
  setHeaderOverride: noop
35
36
  };
@@ -160,10 +161,13 @@ function ToolModalShell({
160
161
  isSearchActive,
161
162
  clearSearch,
162
163
  close: onClose,
164
+ // Bare call: JsModal persists its own dimensions, so restoring brings
165
+ // the tool back the size the user last left it.
166
+ minimize: () => onMinimize(),
163
167
  showToast,
164
168
  setHeaderOverride,
165
169
  instanceId: app.instanceId
166
- }), [search, isSearchActive, clearSearch, onClose, showToast, app.instanceId]);
170
+ }), [search, isSearchActive, clearSearch, onClose, onMinimize, showToast, app.instanceId]);
167
171
  const title = headerOverride?.title ?? config.title ?? app.title ?? app.id;
168
172
  const subtitle = headerOverride?.subtitle ?? config.subtitle;
169
173
  const onBack = headerOverride?.onBack;
@@ -13,12 +13,13 @@ var _appSyncAdapter = require("./sync/appSyncAdapter.js");
13
13
  * Zero-config device→desktop sync for FloatingDevTools.
14
14
  *
15
15
  * When @buoy-gg/external-sync is installed, FloatingDevTools renders
16
- * <AutoExternalSync /> in dev builds: it connects to the Buoy Desktop broker
17
- * (port 42831 on the Metro host, so physical devices reach the dev machine
18
- * automatically — a background retry when the desktop app isn't running) and
19
- * registers a sync adapter for every installed tool package, discovered with
20
- * the same guarded-require pattern as autoDiscoverPresets. No <DevToolsSync>
21
- * wiring needed in the app.
16
+ * <AutoExternalSync /> in dev builds (and in release builds that opted in via
17
+ * externalSync.enableInRelease — see externalSyncGate.ts): it connects to the
18
+ * Buoy Desktop broker (port 42831 on the Metro host, so physical devices reach
19
+ * the dev machine automatically — a background retry when the desktop app
20
+ * isn't running) and registers a sync adapter for every installed tool
21
+ * package, discovered with the same guarded-require pattern as
22
+ * autoDiscoverPresets. No <DevToolsSync> wiring needed in the app.
22
23
  *
23
24
  * Identity defaults come from expo-constants when available (app name →
24
25
  * "MyApp (ios)" / "myapp-ios"), overridable via the `externalSync` prop.
@@ -180,6 +181,7 @@ function AutoExternalSync({
180
181
  options,
181
182
  requiredEnvVars,
182
183
  isPro,
184
+ tier,
183
185
  licenseKey,
184
186
  customApps
185
187
  }) {
@@ -213,10 +215,12 @@ function AutoExternalSync({
213
215
  // record and rebroadcasts, so the desktop dashboard can auto-adopt the key.
214
216
  const licenseRef = (0, _react.useRef)({
215
217
  isPro,
218
+ tier,
216
219
  licenseKey
217
220
  });
218
221
  licenseRef.current = {
219
222
  isPro,
223
+ tier,
220
224
  licenseKey
221
225
  };
222
226
  (0, _react.useEffect)(() => {
@@ -224,6 +228,8 @@ function AutoExternalSync({
224
228
  const emitLicense = () => {
225
229
  socket.emit("device-license", {
226
230
  isPro: licenseRef.current.isPro ? "1" : "0",
231
+ // Brokers older than the tier split ignore this; newer ones prefer it.
232
+ tier: licenseRef.current.tier ?? (licenseRef.current.isPro ? "pro" : "anonymous"),
227
233
  licenseKey: licenseRef.current.licenseKey ?? ""
228
234
  });
229
235
  };
@@ -232,7 +238,7 @@ function AutoExternalSync({
232
238
  return () => {
233
239
  socket.off("connect", emitLicense);
234
240
  };
235
- }, [socket, isPro, licenseKey]);
241
+ }, [socket, isPro, tier, licenseKey]);
236
242
  const tools = (0, _react.useMemo)(() => {
237
243
  // Core's own adapter — device-level capabilities (JS-bundle reload) that
238
244
  // don't belong to any tool package. Registered unconditionally so remote