fix: take the dev tunnel's token from the dev server, not a guessed file

The banner re-derived the token from ~/.fusion/settings.json. That is simply
the wrong source: on a real run the file contained no daemonToken while the
dashboard's own banner, two lines above, printed a working one — so the
tunnel claimed no token existed next to a token that plainly did.

The dashboard already holds the resolved token at the point where it reports
its bound port, so it now reports both over the same IPC message and the
wrapper prefers that over anything it could derive. The env/settings lookup
survives only for targets that report nothing, such as an explicit
--tunnel=PORT aimed at a server the dev child knows nothing about. The token
crosses the existing parent/child channel only; it is never logged or sent
onward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-18 20:16:44 -07:00
parent e4a53b6f47
commit 6f461a4744
6 changed files with 90 additions and 21 deletions

View File

@@ -213,10 +213,23 @@ export const DEV_SERVER_LISTENING_MESSAGE = "fusion:dev-server-listening";
/** Port from a dev child's listening report, or null for any other message. */
export function readDevServerListeningPort(message) {
return readDevServerListening(message)?.port ?? null;
}
/**
* The dev child's listening report: the port it actually bound and the auth token it installed.
*
* FNXC:DevTunnel 2026-08-19-03:00: the token comes from the child because the supervisor cannot
* derive it — reading ~/.fusion/settings.json found nothing on a real run while the dashboard had a
* perfectly good token in memory, so `--tunnel` printed "no token yet" next to a working banner.
*/
export function readDevServerListening(message) {
if (!message || typeof message !== "object") return null;
if (message.type !== DEV_SERVER_LISTENING_MESSAGE) return null;
const port = Number(message.port);
return Number.isInteger(port) && port > 0 ? port : null;
if (!Number.isInteger(port) || port <= 0) return null;
const token = typeof message.token === "string" && message.token.length > 0 ? message.token : null;
return { port, token };
}
/**

View File

@@ -14,7 +14,7 @@ import {
createDevWatchRestartCoordinator,
getPrebuildCommand,
parseDevWrapperArgs,
readDevServerListeningPort,
readDevServerListening,
resolveDevTunnelPort,
resolvePrebuildMode,
} from "./dev-with-memory-lib.mjs";
@@ -110,30 +110,30 @@ nothing about), so it is used immediately and never waits. If the report never a
still comes up on the configured port, since a mis-targeted preview beats no preview at all.
*/
const DEV_SERVER_PORT_REPORT_TIMEOUT_MS = 60_000;
let reportDevServerPort;
const devServerPortReport = new Promise((resolve) => { reportDevServerPort = resolve; });
async function resolveTunnelTargetPort() {
if (tunnelPort) return { port: tunnelPort, source: "explicit" };
let reportDevServerListening;
const devServerListeningReport = new Promise((resolve) => { reportDevServerListening = resolve; });
async function resolveTunnelTarget() {
const configured = resolveDevTunnelPort(undefined);
if (tunnelPort) return { port: tunnelPort, token: null, source: "explicit" };
const timeout = new Promise((resolve) => {
setTimeout(() => resolve(null), DEV_SERVER_PORT_REPORT_TIMEOUT_MS).unref?.();
});
const reported = await Promise.race([devServerPortReport, timeout]);
const reported = await Promise.race([devServerListeningReport, timeout]);
if (reported == null) {
if (!reported) {
console.warn(`[fusion:dev] dev server never reported its port — tunnelling ${configured}, which may not be it`);
return { port: configured, source: "assumed" };
return { port: configured, token: null, source: "assumed" };
}
if (reported !== configured) {
console.log(`[fusion:dev] dev server bound ${reported} (not ${configured}) — tunnelling ${reported}`);
if (reported.port !== configured) {
console.log(`[fusion:dev] dev server bound ${reported.port} (not ${configured}) — tunnelling ${reported.port}`);
}
return { port: reported, source: "reported" };
return { port: reported.port, token: reported.token, source: "reported" };
}
async function openDevTunnel() {
const { port, source } = await resolveTunnelTargetPort();
const { port, token, source } = await resolveTunnelTarget();
/*
FNXC:DevTunnel 2026-08-19-02:05:
A port the CHILD reported is the dev dashboard by definition, whatever number it landed on — so it
@@ -147,7 +147,9 @@ async function openDevTunnel() {
Resolved at print time (not at parse time) so the token the dev child mints on a first
authenticated run is already on disk by the time the banner needs it.
*/
const auth = resolveDevTunnelAuth({ port, dashboardPort, args: forwardedArgs });
// FNXC:DevTunnel 2026-08-19-03:00: the child's own token wins; the settings/env lookup is only a
// fallback for targets that never reported one (an explicit --tunnel=PORT).
const auth = resolveDevTunnelAuth({ port, dashboardPort, args: forwardedArgs, reportedToken: token });
devTunnel = await startDevTunnel({ port, auth });
}
@@ -189,8 +191,8 @@ function runApp(extraArgs) {
}
watchRestart.attach(tsx);
tsx.on("message", (message) => {
const listeningPort = readDevServerListeningPort(message);
if (listeningPort) reportDevServerPort(listeningPort);
const listening = readDevServerListening(message);
if (listening) reportDevServerListening(listening);
watchRestart.onMessage(message);
});
ensureSourceWatcher();

View File

@@ -158,11 +158,20 @@ export function resolveDevTunnelAuth({
env = process.env,
settingsFile = resolveGlobalSettingsFile(),
readToken = readStoredDaemonToken,
reportedToken = null,
} = {}) {
if (port !== dashboardPort) return { kind: "foreign" };
if (args.includes("--no-auth")) return { kind: "no-auth" };
const token = env.FUSION_DASHBOARD_TOKEN
/*
FNXC:DevTunnel 2026-08-19-03:00:
`reportedToken` is the token the dev server actually installed, handed over its IPC channel. It
wins over every derived source because deriving was wrong: on a real run the token was not in
~/.fusion/settings.json at all, so the banner claimed none existed while the dashboard printed a
working one directly above it. The env/file lookup remains for targets that report nothing.
*/
const token = reportedToken
?? env.FUSION_DASHBOARD_TOKEN
?? env.FUSION_DAEMON_TOKEN
?? readToken(settingsFile);