From 161ea9b3ebd4423cb450ed129060c8081bd6e84d Mon Sep 17 00:00:00 2001 From: dadachann <185672915+dadachann@users.noreply.github.com> Date: Sat, 1 Aug 2026 04:41:44 +0000 Subject: [PATCH] fix(cloud): restore fragment-based Space launch callback --- web/src/app/auth/space/callback/page.tsx | 29 +++++++++++++++++-- web/tests/unit/space-launch-callback.test.mjs | 18 ++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 web/tests/unit/space-launch-callback.test.mjs diff --git a/web/src/app/auth/space/callback/page.tsx b/web/src/app/auth/space/callback/page.tsx index 1028de6ac..cfce808fd 100644 --- a/web/src/app/auth/space/callback/page.tsx +++ b/web/src/app/auth/space/callback/page.tsx @@ -66,6 +66,10 @@ function SpaceOAuthCallbackContent() { const [searchParams] = useSearchParams(); const { t } = useTranslation(); const isMountedRef = useRef(true); + const directLaunchFragmentRef = useRef<{ + workspaceUuid: string | null; + launchAssertion: string | null; + } | null>(null); const [status, setStatus] = useState< 'loading' | 'confirm' | 'success' | 'error' @@ -220,8 +224,29 @@ function SpaceOAuthCallbackContent() { const errorDescription = searchParams.get('error_description'); const mode = searchParams.get('mode'); const state = searchParams.get('state'); - const workspaceUuid = searchParams.get('workspace_uuid'); - const launchAssertion = searchParams.get('launch_assertion'); + if (directLaunchFragmentRef.current === null) { + const fragmentParams = new URLSearchParams( + window.location.hash.startsWith('#') + ? window.location.hash.slice(1) + : window.location.hash, + ); + directLaunchFragmentRef.current = { + workspaceUuid: fragmentParams.get('workspace_uuid'), + launchAssertion: fragmentParams.get('launch_assertion'), + }; + if (window.location.hash) { + window.history.replaceState( + null, + '', + `${window.location.pathname}${window.location.search}`, + ); + } + } + const workspaceUuid = + directLaunchFragmentRef.current.workspaceUuid ?? + searchParams.get('workspace_uuid'); + const launchAssertion = + directLaunchFragmentRef.current.launchAssertion; if (error) { setStatus('error'); diff --git a/web/tests/unit/space-launch-callback.test.mjs b/web/tests/unit/space-launch-callback.test.mjs new file mode 100644 index 000000000..ff4ac419c --- /dev/null +++ b/web/tests/unit/space-launch-callback.test.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; + +const source = fs.readFileSync( + new URL('../../src/app/auth/space/callback/page.tsx', import.meta.url), + 'utf8', +); + +test('direct launch assertion is fragment-only and removed before exchange', () => { + assert.doesNotMatch(source, /searchParams\.get\(['"]launch_assertion['"]\)/); + const readIndex = source.indexOf("fragmentParams.get('launch_assertion')"); + const clearIndex = source.indexOf('window.history.replaceState'); + const exchangeIndex = source.indexOf('handleOAuthCallback(', clearIndex); + assert.ok(readIndex >= 0, 'fragment assertion read is missing'); + assert.ok(clearIndex > readIndex, 'URL fragment is not cleared after copying the assertion'); + assert.ok(exchangeIndex > clearIndex, 'assertion exchange starts before the fragment is cleared'); +});