mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
# 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
This commit is contained in:
@@ -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)
|
# v0.4.41 (2026-05-14)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router",
|
"name": "9router",
|
||||||
"version": "0.4.41",
|
"version": "0.4.43",
|
||||||
"description": "9Router CLI - Start and manage 9Router server",
|
"description": "9Router CLI - Start and manage 9Router server",
|
||||||
"bin": {
|
"bin": {
|
||||||
"9router": "./cli.js"
|
"9router": "./cli.js"
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ const buildHomeDir = path.join(cliDir, ".build-home");
|
|||||||
const buildDistDirName = ".next-cli-build";
|
const buildDistDirName = ".next-cli-build";
|
||||||
const buildDistDir = path.join(appDir, buildDistDirName);
|
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
|
// Exclude patterns for files/folders we don't want to copy
|
||||||
const EXCLUDE_PATTERNS = [
|
const EXCLUDE_PATTERNS = [
|
||||||
"@img", // Sharp image processing (not needed with unoptimized images)
|
"@img", // Sharp image processing (not needed with unoptimized images)
|
||||||
@@ -122,7 +114,7 @@ try {
|
|||||||
APPDATA: path.join(buildHomeDir, "AppData", "Roaming"),
|
APPDATA: path.join(buildHomeDir, "AppData", "Roaming"),
|
||||||
LOCALAPPDATA: path.join(buildHomeDir, "AppData", "Local"),
|
LOCALAPPDATA: path.join(buildHomeDir, "AppData", "Local"),
|
||||||
NEXT_DIST_DIR: buildDistDirName,
|
NEXT_DIST_DIR: buildDistDirName,
|
||||||
NEXT_TRACING_ROOT_MODE: shouldUseWorkspaceTracingRoot() ? "workspace" : "project",
|
NEXT_TRACING_ROOT_MODE: "workspace",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("✅ Next.js build completed\n");
|
console.log("✅ Next.js build completed\n");
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.4.41",
|
"version": "0.4.43",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -343,13 +343,6 @@ export default function CoworkToolCard({
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => setModelSelectOpen(true)}
|
|
||||||
disabled={!hasActiveProviders}
|
|
||||||
className={`shrink-0 px-2 py-1.5 rounded border text-xs whitespace-nowrap transition-colors ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
|
||||||
>
|
|
||||||
Select Model
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setComboModalOpen(true)} disabled={!hasActiveProviders} className={`shrink-0 px-2 py-1.5 rounded border text-xs whitespace-nowrap transition-colors ${hasActiveProviders ? "bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>+ Combo</button>
|
<button onClick={() => setComboModalOpen(true)} disabled={!hasActiveProviders} className={`shrink-0 px-2 py-1.5 rounded border text-xs whitespace-nowrap transition-colors ${hasActiveProviders ? "bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>+ Combo</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,29 @@ import crypto from "crypto";
|
|||||||
import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
|
import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
|
||||||
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
||||||
import { DATA_DIR } from "@/lib/dataDir";
|
import { DATA_DIR } from "@/lib/dataDir";
|
||||||
|
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||||
|
|
||||||
const APP_PORT = UPDATER_CONFIG.appPort;
|
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";
|
const PROVIDER = "gateway";
|
||||||
|
|
||||||
@@ -328,8 +349,8 @@ export async function POST(request) {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const bridgeEntries = buildLocalBridgeEntries(localPluginNames);
|
const bridgeEntries = await injectAuthHeaders(buildLocalBridgeEntries(localPluginNames));
|
||||||
const customEntries = buildCustomEntries(customPluginsArray);
|
const customEntries = await injectAuthHeaders(buildCustomEntries(customPluginsArray));
|
||||||
const managedMcpServers = [...buildManagedMcpServers(pluginsArray), ...bridgeEntries, ...customEntries];
|
const managedMcpServers = [...buildManagedMcpServers(pluginsArray), ...bridgeEntries, ...customEntries];
|
||||||
|
|
||||||
const bootstrapped = await bootstrapDeploymentMode();
|
const bootstrapped = await bootstrapDeploymentMode();
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ const PROTECTED_API_PATHS = [
|
|||||||
"/api/cli-tools",
|
"/api/cli-tools",
|
||||||
"/api/mcp",
|
"/api/mcp",
|
||||||
"/api/translator",
|
"/api/translator",
|
||||||
|
"/api/tunnel",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Routes that spawn child processes — restrict to localhost regardless of auth.
|
// Routes that spawn child processes — restrict to localhost regardless of auth.
|
||||||
const LOCAL_ONLY_PATHS = [
|
const LOCAL_ONLY_PATHS = [
|
||||||
"/api/cli-tools/cowork-settings",
|
"/api/cli-tools/cowork-settings",
|
||||||
"/api/mcp/",
|
"/api/mcp/",
|
||||||
|
"/api/tunnel/tailscale-install",
|
||||||
];
|
];
|
||||||
|
|
||||||
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
|
import crypto from "crypto";
|
||||||
import { execSync, exec, spawn } from "child_process";
|
import { execSync, exec, spawn } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { execWithPassword } from "@/mitm/dns/dnsConfig";
|
import { execWithPassword } from "@/mitm/dns/dnsConfig";
|
||||||
@@ -302,6 +303,10 @@ async function installTailscaleMac(sudoPassword, log) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function installTailscaleLinux(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...");
|
log("Downloading install script...");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], {
|
const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], {
|
||||||
@@ -315,7 +320,15 @@ async function installTailscaleLinux(sudoPassword, log) {
|
|||||||
curlChild.on("exit", (code) => {
|
curlChild.on("exit", (code) => {
|
||||||
if (code !== 0) return reject(new Error(`Failed to download install script: ${curlErr}`));
|
if (code !== 0) return reject(new Error(`Failed to download install script: ${curlErr}`));
|
||||||
log("Running install script...");
|
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 = "";
|
let stderr = "";
|
||||||
child.stdout.on("data", (d) => {
|
child.stdout.on("data", (d) => {
|
||||||
const line = d.toString().trim();
|
const line = d.toString().trim();
|
||||||
@@ -323,6 +336,7 @@ async function installTailscaleLinux(sudoPassword, log) {
|
|||||||
});
|
});
|
||||||
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
||||||
child.on("close", (c) => {
|
child.on("close", (c) => {
|
||||||
|
cleanup();
|
||||||
if (c === 0) resolve();
|
if (c === 0) resolve();
|
||||||
else {
|
else {
|
||||||
const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry"))
|
const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry"))
|
||||||
@@ -331,9 +345,8 @@ async function installTailscaleLinux(sudoPassword, log) {
|
|||||||
reject(new Error(msg));
|
reject(new Error(msg));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
child.on("error", reject);
|
child.on("error", (e) => { cleanup(); reject(e); });
|
||||||
child.stdin.write(`${sudoPassword}\n`);
|
child.stdin.write(`${sudoPassword}\n`);
|
||||||
child.stdin.write(scriptContent);
|
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
});
|
});
|
||||||
curlChild.on("error", reject);
|
curlChild.on("error", reject);
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ export const config = {
|
|||||||
"/api/cli-tools/:path*",
|
"/api/cli-tools/:path*",
|
||||||
"/api/mcp/:path*",
|
"/api/mcp/:path*",
|
||||||
"/api/translator/:path*",
|
"/api/translator/:path*",
|
||||||
|
"/api/tunnel/:path*",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user