diff --git a/CHANGELOG.md b/CHANGELOG.md index 119eac0..160a2ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to Mission Control are documented in this file. +## [2.0.1] - 2026-03-13 + +### Fixed +- HTTP and Tailscale login broken by unconditional HTTPS redirect — replaced with opt-in `NEXT_PUBLIC_FORCE_HTTPS=1` (#309) +- CSP nonce mismatch blocking inline scripts after login — nonce now propagated into SSR request headers (#308, #311) +- Layout inline theme script missing `nonce` attribute, causing CSP violations on chunk loading (#308, #311) +- Session cookie `Secure` flag forced in production even over HTTP — now derived from actual request protocol (#304) +- Node 24 compatibility alongside Node 22 (#303) + +### Changed +- CSP generation and browser-security helpers extracted to `src/lib/csp.ts` and `src/lib/browser-security.ts` + +### Contributors +- @0xNyk +- @polaris-dxz + ## [2.0.0] - 2026-03-11 ### Added diff --git a/src/app/[[...panel]]/page.tsx b/src/app/[[...panel]]/page.tsx index 308a52d..16e7f23 100644 --- a/src/app/[[...panel]]/page.tsx +++ b/src/app/[[...panel]]/page.tsx @@ -38,6 +38,7 @@ import { ExecApprovalPanel } from '@/components/panels/exec-approval-panel' import { ChatPagePanel } from '@/components/panels/chat-page-panel' import { ChatPanel } from '@/components/chat/chat-panel' import { getPluginPanel } from '@/lib/plugins' +import { shouldRedirectDashboardToHttps } from '@/lib/browser-security' import { ErrorBoundary } from '@/components/ErrorBoundary' import { LocalModeBanner } from '@/components/layout/local-mode-banner' import { UpdateBanner } from '@/components/layout/update-banner' @@ -65,10 +66,6 @@ function renderPluginPanel(panelId: string) { return pluginPanel ? createElement(pluginPanel) : } -function isLocalHost(hostname: string): boolean { - return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' -} - export default function Home() { const router = useRouter() const { connect } = useWebSocket() @@ -148,9 +145,11 @@ export default function Home() { useEffect(() => { setIsClient(true) - // OpenClaw control-ui device identity requires a secure browser context. - // Redirect remote HTTP sessions to HTTPS automatically to avoid handshake failures. - if (window.location.protocol === 'http:' && !isLocalHost(window.location.hostname)) { + if (shouldRedirectDashboardToHttps({ + protocol: window.location.protocol, + hostname: window.location.hostname, + forceHttps: process.env.NEXT_PUBLIC_FORCE_HTTPS === '1', + })) { const secureUrl = new URL(window.location.href) secureUrl.protocol = 'https:' window.location.replace(secureUrl.toString()) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b585218..ed9ee18 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from 'next' import { Inter, JetBrains_Mono } from 'next/font/google' +import { headers } from 'next/headers' import { ThemeProvider } from 'next-themes' import { THEME_IDS } from '@/lib/themes' import { ThemeBackground } from '@/components/ui/theme-background' @@ -78,17 +79,20 @@ export const metadata: Metadata = { }, } -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode }) { + const nonce = (await headers()).get('x-nonce') || undefined + return ( {/* Blocking script to set 'dark' class before first paint, preventing FOUC. Content is a static string literal — no user input, no XSS vector. */}