From 9b044de8669dedfc534819221cc67585624b87e3 Mon Sep 17 00:00:00 2001 From: 47Seek <229621367+47seek@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:55:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E5=B9=B3=E5=8F=B0=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E8=87=AA=E5=8A=A8=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dashboard.ts | 12 +++++++++--- src/dashboard/auth.ts | 9 +++++++++ test/dashboard-auth.test.ts | 16 +++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/dashboard.ts b/src/dashboard.ts index b612de4fd..fa48b222c 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -15,7 +15,8 @@ import { config } from './config.js'; import { listenWithProbe } from './utils/listen-with-probe.js'; import { generateToken, parseCookie, buildSetCookie, verifyHmac, cliAuthBind, decideDashboardAuth, - loadPersistedToken, persistToken, loadDashboardSecret, loadOrCreateDashboardSecret, + loadPersistedToken, loadOrCreatePersistedToken, persistToken, + loadDashboardSecret, loadOrCreateDashboardSecret, } from './dashboard/auth.js'; import { DaemonRegistry, botsRosterSignature } from './dashboard/registry.js'; import { Aggregator, subscribeDaemon } from './dashboard/aggregator.js'; @@ -232,8 +233,9 @@ function loadOrCreateSecret(): string { } // The active dashboard token is persisted to disk so a previously-issued -// dashboard URL survives `botmux restart`; only `botmux dashboard` (the -// /__cli/rotate endpoint) rotates it and thereby invalidates the old link. +// dashboard URL survives `botmux restart`. A platform-bound dashboard creates +// the first token on startup; only `botmux dashboard` (the /__cli/rotate +// endpoint) replaces it and thereby invalidates the old link. // The start/restart hint reads it via the non-rotating /__cli/current endpoint // so it can show the live link without invalidating it. let activeToken: string | null = loadPersistedToken(TOKEN_PATH); @@ -5189,6 +5191,10 @@ function startPlatformTunnelIfBound(): void { try { const binding = readPlatformBinding(); if (!binding) return; + if (!activeToken) { + activeToken = loadOrCreatePersistedToken(TOKEN_PATH); + logger.info('[platform-tunnel] 已初始化 dashboard token'); + } const version = readBotmuxVersion(); platformTunnel = startPlatformTunnelClient({ binding, diff --git a/src/dashboard/auth.ts b/src/dashboard/auth.ts index 1a317f28c..4fa87ce90 100644 --- a/src/dashboard/auth.ts +++ b/src/dashboard/auth.ts @@ -172,6 +172,15 @@ export function persistToken(tokenPath: string, token: string): void { chmodSync(tokenPath, 0o600); } +/** Load the active token, creating and persisting the first one when absent. */ +export function loadOrCreatePersistedToken(tokenPath: string): string { + const existing = loadPersistedToken(tokenPath); + if (existing) return existing; + const token = generateToken(); + persistToken(tokenPath, token); + return token; +} + /** Extract `botmux_dashboard_token` value from a Cookie header. */ export function parseCookie(header: string | undefined): string | undefined { if (!header) return undefined; diff --git a/test/dashboard-auth.test.ts b/test/dashboard-auth.test.ts index f39fd015a..fba6c4967 100644 --- a/test/dashboard-auth.test.ts +++ b/test/dashboard-auth.test.ts @@ -9,7 +9,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { verifyHmac, generateToken, parseCookie, decideDashboardAuth, - loadPersistedToken, persistToken, loadDashboardSecret, loadOrCreateDashboardSecret, + loadPersistedToken, loadOrCreatePersistedToken, persistToken, + loadDashboardSecret, loadOrCreateDashboardSecret, } from '../src/dashboard/auth.js'; const SECRET = 'a'.repeat(43); // base64url 32 bytes @@ -83,6 +84,19 @@ describe('token persistence (survives restart, rotates only on `botmux dashboard expect(loadPersistedToken(tokenPath)).toBe(tok); }); + it('loadOrCreatePersistedToken creates and persists a missing token', () => { + const tok = loadOrCreatePersistedToken(tokenPath); + expect(tok).toMatch(/^[A-Za-z0-9_-]{43}$/); + expect(loadPersistedToken(tokenPath)).toBe(tok); + expect(statSync(tokenPath).mode & 0o777).toBe(0o600); + }); + + it('loadOrCreatePersistedToken reuses an existing token without rotating it', () => { + persistToken(tokenPath, 'existing-token'); + expect(loadOrCreatePersistedToken(tokenPath)).toBe('existing-token'); + expect(loadPersistedToken(tokenPath)).toBe('existing-token'); + }); + it('persisted token survives a simulated restart (same file, new process)', () => { const tok = generateToken(); persistToken(tokenPath, tok);