Files
waggle-os/scripts/read-tauri-bootstrap-token.mjs
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

263 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
import process from 'node:process';
import WebSocket from 'ws';
const DEFAULT_TIMEOUT_MS = 60_000;
const MIN_TOKEN_LENGTH = 32;
const MAX_TOKEN_LENGTH = 200;
function fail(message) {
console.error(`tauri-bootstrap-token: ${message}`);
process.exitCode = 1;
}
function parseArgs(argv) {
const options = {
port: null,
timeoutMs: DEFAULT_TIMEOUT_MS,
selfTest: false,
allowLegacyUi: false,
};
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === '--self-test') {
options.selfTest = true;
continue;
}
if (argument === '--allow-legacy-ui') {
options.allowLegacyUi = true;
continue;
}
if (argument === '--port' || argument === '--timeout-ms') {
const value = argv[index + 1];
index += 1;
if (value === undefined) throw new Error(`${argument} requires a value`);
if (argument === '--port') options.port = Number(value);
else options.timeoutMs = Number(value);
continue;
}
if (argument.startsWith('--port=')) options.port = Number(argument.slice(7));
else if (argument.startsWith('--timeout-ms=')) options.timeoutMs = Number(argument.slice(13));
else throw new Error(`unknown argument: ${argument}`);
}
if (!Number.isInteger(options.port) || options.port < 1024 || options.port > 65535) {
throw new Error('port must be an integer between 1024 and 65535');
}
if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1000 || options.timeoutMs > 300_000) {
throw new Error('timeout-ms must be an integer between 1000 and 300000');
}
return options;
}
function isValidEndpoint(value) {
return value !== null
&& typeof value === 'object'
&& Number.isInteger(value.port)
&& value.port > 0
&& value.port <= 65535
&& typeof value.instanceId === 'string'
&& value.instanceId.trim().length > 0
&& typeof value.bootstrapToken === 'string'
&& value.bootstrapToken.length >= MIN_TOKEN_LENGTH
&& value.bootstrapToken.length <= MAX_TOKEN_LENGTH;
}
function isValidDesktopRuntime(value, allowLegacyUi = false) {
return isValidEndpoint(value)
&& value.uiReady === true
&& value.uiStartupState === (allowLegacyUi ? 'legacy-ready' : 'ready')
&& typeof value.uiPath === 'string'
&& value.uiPath.startsWith('/')
&& Number.isInteger(value.uiTextLength)
&& value.uiTextLength > 0;
}
async function fetchTargets(port) {
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
if (!response.ok) throw new Error(`WebView debug endpoint returned HTTP ${response.status}`);
const targets = await response.json();
if (!Array.isArray(targets)) throw new Error('WebView debug endpoint returned an invalid target list');
return targets.filter((target) => target?.type === 'page' && typeof target.webSocketDebuggerUrl === 'string');
}
async function evaluateEnsureService(webSocketUrl, timeoutMs, allowLegacyUi) {
const socket = new WebSocket(webSocketUrl);
let nextId = 1;
const pending = new Map();
const timer = setTimeout(() => {
for (const pendingRequest of pending.values()) pendingRequest.reject(new Error('CDP request timed out'));
pending.clear();
socket.close();
}, timeoutMs);
try {
await new Promise((resolve, reject) => {
const openTimer = setTimeout(() => reject(new Error('CDP WebSocket open timed out')), timeoutMs);
socket.addEventListener('open', () => {
clearTimeout(openTimer);
resolve();
}, { once: true });
socket.addEventListener('error', () => {
clearTimeout(openTimer);
reject(new Error('CDP WebSocket connection failed'));
}, { once: true });
});
socket.addEventListener('message', (event) => {
let message;
try {
message = JSON.parse(String(event.data));
} catch {
return;
}
const request = pending.get(message.id);
if (!request) return;
pending.delete(message.id);
if (message.error) request.reject(new Error('Tauri IPC evaluation failed'));
else request.resolve(message.result);
});
const result = await new Promise((resolve, reject) => {
const id = nextId;
nextId += 1;
pending.set(id, { resolve, reject });
socket.send(JSON.stringify({
id,
method: 'Runtime.evaluate',
params: {
awaitPromise: true,
returnByValue: true,
expression: `
(async () => {
const invoke = globalThis.__TAURI_INTERNALS__?.invoke;
if (typeof invoke !== 'function') throw new Error('Tauri IPC is unavailable');
const endpoint = await invoke('ensure_service');
const deadline = Date.now() + 10_000;
for (;;) {
const root = document.getElementById('root');
const startup = root?.querySelector('[data-waggle-startup]');
const startupState = startup?.getAttribute('data-waggle-startup') ?? null;
const readyState = root?.getAttribute('data-waggle-ui-ready') ?? null;
const uiTextLength = root?.innerText?.trim().length ?? 0;
const strictUiReady = Boolean(
root
&& root.childElementCount > 0
&& !startup
&& readyState === 'ready'
&& uiTextLength > 0
);
const legacyUiReady = Boolean(
root
&& root.childElementCount > 0
&& !startup
&& uiTextLength > 0
);
const uiReady = ${allowLegacyUi ? 'legacyUiReady' : 'strictUiReady'};
if (uiReady || startupState === 'failed' || Date.now() >= deadline) {
return {
...endpoint,
uiPath: globalThis.location?.pathname ?? '',
uiReady,
uiStartupState: ${allowLegacyUi ? "uiReady ? 'legacy-ready' : (readyState ?? startupState)" : 'readyState ?? startupState'},
uiTextLength,
};
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
})()
`,
},
}));
});
const remote = result?.result?.value;
if (!isValidEndpoint(remote)) throw new Error('Tauri returned an invalid service endpoint');
if (!isValidDesktopRuntime(remote, allowLegacyUi)) {
throw new Error('Waggle UI did not render its application shell');
}
return {
bootstrapToken: remote.bootstrapToken,
uiPath: remote.uiPath,
uiReady: remote.uiReady,
uiStartupState: remote.uiStartupState,
uiTextLength: remote.uiTextLength,
};
} finally {
clearTimeout(timer);
socket.close();
}
}
async function resolveToken(port, timeoutMs, allowLegacyUi) {
const deadline = Date.now() + timeoutMs;
let lastError = 'WebView target not ready';
while (Date.now() < deadline) {
try {
const targets = await fetchTargets(port);
for (const target of targets) {
try {
return await evaluateEnsureService(
target.webSocketDebuggerUrl,
Math.min(15_000, deadline - Date.now()),
allowLegacyUi,
);
} catch (error) {
lastError = error instanceof Error ? error.message : 'CDP evaluation failed';
}
}
} catch (error) {
lastError = error instanceof Error ? error.message : 'WebView debug endpoint unavailable';
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(lastError);
}
async function main() {
if (process.argv.includes('--self-test')) {
if (typeof WebSocket !== 'function') throw new Error('WebSocket client is unavailable');
const valid = 'a'.repeat(MIN_TOKEN_LENGTH);
if (!isValidEndpoint({ port: 3333, instanceId: 'test-instance', bootstrapToken: valid })) {
throw new Error('valid endpoint fixture rejected');
}
if (isValidEndpoint({ port: 3333, instanceId: 'test-instance', bootstrapToken: 'short' })) {
throw new Error('short token fixture accepted');
}
const validRuntime = {
port: 3333,
instanceId: 'test-instance',
bootstrapToken: valid,
uiPath: '/home',
uiReady: true,
uiStartupState: 'ready',
uiTextLength: 10,
};
if (!isValidDesktopRuntime(validRuntime)) throw new Error('valid desktop runtime rejected');
if (isValidDesktopRuntime({ ...validRuntime, uiReady: false })) {
throw new Error('blank desktop runtime accepted');
}
if (isValidDesktopRuntime({ ...validRuntime, uiTextLength: 0 })) {
throw new Error('empty desktop runtime accepted');
}
if (isValidDesktopRuntime({ ...validRuntime, uiStartupState: 'loading' })) {
throw new Error('loading desktop runtime accepted');
}
if (isValidDesktopRuntime({ ...validRuntime, uiStartupState: 'failed' })) {
throw new Error('failed desktop runtime accepted');
}
if (!isValidDesktopRuntime({ ...validRuntime, uiStartupState: 'legacy-ready' }, true)) {
throw new Error('valid legacy desktop runtime rejected');
}
if (isValidDesktopRuntime({ ...validRuntime, uiReady: false, uiStartupState: 'legacy-ready' }, true)) {
throw new Error('blank legacy desktop runtime accepted');
}
console.log(JSON.stringify({ pass: true, cases: 9 }));
return;
}
const options = parseArgs(process.argv.slice(2));
const runtime = await resolveToken(options.port, options.timeoutMs, options.allowLegacyUi);
console.log(JSON.stringify(runtime));
}
main().catch((error) => fail(error instanceof Error ? error.message : 'unknown failure'));