@buoy-gg/core 7.0.22 → 7.0.24
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/buoy.js +300 -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/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/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/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/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
package/bin/buoy.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
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
|
+
npx --package=@buoy-gg/core buoy <command> ${dim('(if npx picks the wrong one)')}
|
|
268
|
+
|
|
269
|
+
${bold('Commands')}
|
|
270
|
+
login Sign in and write your license key to .env.local
|
|
271
|
+
help Show this message
|
|
272
|
+
|
|
273
|
+
${bold('Notes')}
|
|
274
|
+
A free key takes about thirty seconds and needs no card. Paid keys use the
|
|
275
|
+
same command - it writes whichever key your account already holds.
|
|
276
|
+
|
|
277
|
+
Docs: ${SITE}/buoy/latest/docs/installation
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Only dispatch when run as a program. Without this guard, importing the file
|
|
282
|
+
// in a test would parse the test runner's own argv and exit the process.
|
|
283
|
+
if (require.main === module) {
|
|
284
|
+
const command = process.argv[2];
|
|
285
|
+
|
|
286
|
+
if (command === 'login') {
|
|
287
|
+
login();
|
|
288
|
+
} else if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
289
|
+
help();
|
|
290
|
+
} else {
|
|
291
|
+
fail(`Unknown command: ${command}\n Run \`npx buoy help\` to see what is available.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
detectEnvVarName,
|
|
297
|
+
writeEnvVar,
|
|
298
|
+
ensureGitignored,
|
|
299
|
+
awaitKeyFromBrowser,
|
|
300
|
+
};
|
package/lib/commonjs/Buoy.js
CHANGED
|
@@ -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:
|
|
383
|
-
//
|
|
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
|
-
//
|
|
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
|
|
394
|
-
// contacted
|
|
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, {}),
|
|
398
|
-
options:
|
|
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, {}),
|
|
439
|
-
options:
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
@@ -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
|
+
}
|