mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
fix(cloud): make direct launch replay-safe
This commit is contained in:
@@ -29,6 +29,7 @@ type SpaceOAuthLoginResult = {
|
||||
token: string;
|
||||
user: string;
|
||||
workspace_uuid?: string;
|
||||
return_path?: string;
|
||||
};
|
||||
|
||||
const pendingSpaceOAuthLogins = new Map<
|
||||
@@ -63,6 +64,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'
|
||||
@@ -106,7 +111,13 @@ function SpaceOAuthCallbackContent() {
|
||||
throw new Error('No Workspace is available for this Account');
|
||||
}
|
||||
if (response.workspace_uuid) {
|
||||
navigate('/home', { replace: true });
|
||||
const returnPath =
|
||||
typeof response.return_path === 'string' &&
|
||||
response.return_path.startsWith('/') &&
|
||||
!response.return_path.startsWith('//')
|
||||
? response.return_path
|
||||
: '/home';
|
||||
navigate(returnPath, { replace: true });
|
||||
return;
|
||||
}
|
||||
setStatus('success');
|
||||
@@ -207,16 +218,29 @@ function SpaceOAuthCallbackContent() {
|
||||
const errorDescription = searchParams.get('error_description');
|
||||
const mode = searchParams.get('mode');
|
||||
const state = searchParams.get('state');
|
||||
const fragmentParams = new URLSearchParams(
|
||||
window.location.hash.startsWith('#')
|
||||
? window.location.hash.slice(1)
|
||||
: window.location.hash,
|
||||
);
|
||||
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 =
|
||||
fragmentParams.get('workspace_uuid') ?? searchParams.get('workspace_uuid');
|
||||
directLaunchFragmentRef.current.workspaceUuid ??
|
||||
searchParams.get('workspace_uuid');
|
||||
const launchAssertion =
|
||||
fragmentParams.get('launch_assertion') ??
|
||||
searchParams.get('launch_assertion');
|
||||
directLaunchFragmentRef.current.launchAssertion;
|
||||
|
||||
if (error) {
|
||||
setStatus('error');
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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');
|
||||
});
|
||||
|
||||
test('direct launch honors only a local signed return path', () => {
|
||||
assert.match(source, /response\.return_path\.startsWith\(['"]\/['"]\)/);
|
||||
assert.match(source, /!response\.return_path\.startsWith\(['"]\/\/['"]\)/);
|
||||
assert.match(source, /navigate\(returnPath, \{ replace: true \}\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user