From a28c5ec98b58d685b9855bf429f07d780312c8f6 Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 15 May 2026 12:02:32 +0700 Subject: [PATCH] # v0.4.44 (2026-05-15) ## Features - Add Blackbox provider with `bb` alias (#1143) - Add Xiaomi token plan provider - Enhance model select modal UX + modal traffic lights (#1111) - Default Usage dashboard period to Today (#1141) ## Fixes - Fix Cowork model selection and Windows CLI packaging (#1129) - Update provider name retrieval for compatibility provider (#1135) - Update JWT_SECRET handling --- CHANGELOG.md | 13 ++++++++++ cli/package.json | 2 +- cli/scripts/build-cli.js | 10 +------- package.json | 2 +- .../cli-tools/components/CoworkToolCard.js | 7 ------ .../api/cli-tools/cowork-settings/route.js | 25 +++++++++++++++++-- src/dashboardGuard.js | 2 ++ src/lib/tunnel/tailscale.js | 19 +++++++++++--- src/proxy.js | 1 + 9 files changed, 58 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d43c531..4cc0585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# v0.4.43 (2026-05-15) + +## Features +- Add Blackbox provider with `bb` alias (#1143) +- Add Xiaomi token plan provider +- Enhance model select modal UX + modal traffic lights (#1111) +- Default Usage dashboard period to Today (#1141) + +## Fixes +- Fix Cowork model selection and Windows CLI packaging (#1129) +- Update provider name retrieval for compatibility provider (#1135) +- Update JWT_SECRET handling + # v0.4.41 (2026-05-14) ## Features diff --git a/cli/package.json b/cli/package.json index aad50da..4ebef16 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "9router", - "version": "0.4.41", + "version": "0.4.43", "description": "9Router CLI - Start and manage 9Router server", "bin": { "9router": "./cli.js" diff --git a/cli/scripts/build-cli.js b/cli/scripts/build-cli.js index 4b5443f..760e1e8 100644 --- a/cli/scripts/build-cli.js +++ b/cli/scripts/build-cli.js @@ -12,14 +12,6 @@ const buildHomeDir = path.join(cliDir, ".build-home"); const buildDistDirName = ".next-cli-build"; const buildDistDir = path.join(appDir, buildDistDirName); -function shouldUseWorkspaceTracingRoot() { - const appNodeModules = path.join(appDir, "node_modules"); - const rootNodeModules = path.join(rootDir, "node_modules"); - - // Only widen tracing when dependencies are actually hoisted above appDir. - return !fs.existsSync(appNodeModules) && fs.existsSync(rootNodeModules); -} - // Exclude patterns for files/folders we don't want to copy const EXCLUDE_PATTERNS = [ "@img", // Sharp image processing (not needed with unoptimized images) @@ -122,7 +114,7 @@ try { APPDATA: path.join(buildHomeDir, "AppData", "Roaming"), LOCALAPPDATA: path.join(buildHomeDir, "AppData", "Local"), NEXT_DIST_DIR: buildDistDirName, - NEXT_TRACING_ROOT_MODE: shouldUseWorkspaceTracingRoot() ? "workspace" : "project", + NEXT_TRACING_ROOT_MODE: "workspace", } }); console.log("✅ Next.js build completed\n"); diff --git a/package.json b/package.json index 79b91f8..5ed0ca8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.4.41", + "version": "0.4.43", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js index 01d1aaa..a3b20bc 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js @@ -343,13 +343,6 @@ export default function CoworkToolCard({ )) )} - diff --git a/src/app/api/cli-tools/cowork-settings/route.js b/src/app/api/cli-tools/cowork-settings/route.js index 6fcf418..25fb237 100644 --- a/src/app/api/cli-tools/cowork-settings/route.js +++ b/src/app/api/cli-tools/cowork-settings/route.js @@ -8,8 +8,29 @@ import crypto from "crypto"; import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins"; import { UPDATER_CONFIG } from "@/shared/constants/config"; import { DATA_DIR } from "@/lib/dataDir"; +import { getConsistentMachineId } from "@/shared/utils/machineId"; const APP_PORT = UPDATER_CONFIG.appPort; +const CLI_TOKEN_HEADER = "x-9r-cli-token"; +const CLI_TOKEN_SALT = "9r-cli-auth"; +const LOCAL_MCP_PREFIX = `http://localhost:${APP_PORT}/api/mcp/`; + +let cachedCliToken = null; +const getCliToken = async () => { + if (!cachedCliToken) cachedCliToken = await getConsistentMachineId(CLI_TOKEN_SALT); + return cachedCliToken; +}; + +// Inject CLI token header into entries pointing at our local /api/mcp/ bridge. +const injectAuthHeaders = async (entries) => { + const token = await getCliToken(); + for (const e of entries) { + if (typeof e?.url === "string" && e.url.startsWith(LOCAL_MCP_PREFIX)) { + e.headers = { ...(e.headers || {}), [CLI_TOKEN_HEADER]: token }; + } + } + return entries; +}; const PROVIDER = "gateway"; @@ -328,8 +349,8 @@ export async function POST(request) { } catch { /* ignore */ } } - const bridgeEntries = buildLocalBridgeEntries(localPluginNames); - const customEntries = buildCustomEntries(customPluginsArray); + const bridgeEntries = await injectAuthHeaders(buildLocalBridgeEntries(localPluginNames)); + const customEntries = await injectAuthHeaders(buildCustomEntries(customPluginsArray)); const managedMcpServers = [...buildManagedMcpServers(pluginsArray), ...bridgeEntries, ...customEntries]; const bootstrapped = await bootstrapDeploymentMode(); diff --git a/src/dashboardGuard.js b/src/dashboardGuard.js index 38681b6..4f7f0ef 100644 --- a/src/dashboardGuard.js +++ b/src/dashboardGuard.js @@ -33,12 +33,14 @@ const PROTECTED_API_PATHS = [ "/api/cli-tools", "/api/mcp", "/api/translator", + "/api/tunnel", ]; // Routes that spawn child processes — restrict to localhost regardless of auth. const LOCAL_ONLY_PATHS = [ "/api/cli-tools/cowork-settings", "/api/mcp/", + "/api/tunnel/tailscale-install", ]; const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); diff --git a/src/lib/tunnel/tailscale.js b/src/lib/tunnel/tailscale.js index d943567..78734c5 100644 --- a/src/lib/tunnel/tailscale.js +++ b/src/lib/tunnel/tailscale.js @@ -1,6 +1,7 @@ import fs from "fs"; import path from "path"; import os from "os"; +import crypto from "crypto"; import { execSync, exec, spawn } from "child_process"; import { promisify } from "util"; import { execWithPassword } from "@/mitm/dns/dnsConfig"; @@ -302,6 +303,10 @@ async function installTailscaleMac(sudoPassword, log) { } async function installTailscaleLinux(sudoPassword, log) { + // Reject password containing newline → prevents stdin command injection + if (typeof sudoPassword !== "string" || sudoPassword.includes("\n")) { + throw new Error("Invalid sudo password"); + } log("Downloading install script..."); return new Promise((resolve, reject) => { const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], { @@ -315,7 +320,15 @@ async function installTailscaleLinux(sudoPassword, log) { curlChild.on("exit", (code) => { if (code !== 0) return reject(new Error(`Failed to download install script: ${curlErr}`)); log("Running install script..."); - const child = spawn("sudo", ["-S", "sh"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); + // Persist script to temp file → exec by path (NOT via stdin) → sh never reads attacker-controlled stdin + const tmpScript = path.join(os.tmpdir(), `tailscale-install-${crypto.randomBytes(8).toString("hex")}.sh`); + try { + fs.writeFileSync(tmpScript, scriptContent, { mode: 0o700 }); + } catch (e) { + return reject(new Error(`Failed to write install script: ${e.message}`)); + } + const cleanup = () => { try { fs.unlinkSync(tmpScript); } catch {} }; + const child = spawn("sudo", ["-S", "sh", tmpScript], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); let stderr = ""; child.stdout.on("data", (d) => { const line = d.toString().trim(); @@ -323,6 +336,7 @@ async function installTailscaleLinux(sudoPassword, log) { }); child.stderr.on("data", (d) => { stderr += d.toString(); }); child.on("close", (c) => { + cleanup(); if (c === 0) resolve(); else { const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry")) @@ -331,9 +345,8 @@ async function installTailscaleLinux(sudoPassword, log) { reject(new Error(msg)); } }); - child.on("error", reject); + child.on("error", (e) => { cleanup(); reject(e); }); child.stdin.write(`${sudoPassword}\n`); - child.stdin.write(scriptContent); child.stdin.end(); }); curlChild.on("error", reject); diff --git a/src/proxy.js b/src/proxy.js index 59917b7..e92e5fd 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -13,5 +13,6 @@ export const config = { "/api/cli-tools/:path*", "/api/mcp/:path*", "/api/translator/:path*", + "/api/tunnel/:path*", ], };