From 9485bda59cc91e6c50dbe3ffbca0766e4d430637 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:23:07 +0000 Subject: [PATCH 1/5] feat: auto-fill auth code when opening KiloClaw gateway Eliminate the manual copy-paste step when accessing claw.kilosessions.ai. The Open button now generates a fresh access code and embeds it as an auth_code query parameter in the gateway URL. The worker validates it on GET and sets the auth cookie directly, falling back to the manual form if the code is invalid or expired. --- kiloclaw/src/routes/access-gateway.ts | 106 +++++++++++------- .../claw/components/AccessCodeActions.tsx | 39 ++++++- src/app/(app)/claw/hooks/useAccessCode.ts | 4 +- 3 files changed, 101 insertions(+), 48 deletions(-) diff --git a/kiloclaw/src/routes/access-gateway.ts b/kiloclaw/src/routes/access-gateway.ts index 1ebc745d3..a3e9fc23d 100644 --- a/kiloclaw/src/routes/access-gateway.ts +++ b/kiloclaw/src/routes/access-gateway.ts @@ -1,4 +1,4 @@ -import { Hono } from 'hono'; +import { type Context, Hono } from 'hono'; import { getCookie, setCookie } from 'hono/cookie'; import type { AppEnv } from '../types'; import { KILOCLAW_AUTH_COOKIE, KILOCLAW_AUTH_COOKIE_MAX_AGE } from '../config'; @@ -159,63 +159,40 @@ async function hasValidCookie( return result.success && result.userId === userId; } -accessGatewayRoutes.get('/kilo-access-gateway', async c => { - const userId = c.req.query('userId'); - if (!userId) { - return c.text('Missing userId parameter', 400); - } - - // If the user already has a valid cookie, derive the gateway token and redirect - const secret = c.env.NEXTAUTH_SECRET; - if (secret) { - const cookie = getCookie(c, KILOCLAW_AUTH_COOKIE); - if (await hasValidCookie(cookie, userId, secret, c.env.WORKER_ENV)) { - const redirectUrl = await buildRedirectUrl(userId, c.env.GATEWAY_TOKEN_SECRET); - return c.redirect(redirectUrl); - } - } - - return c.html(renderPage({ userId })); -}); - -accessGatewayRoutes.post('/kilo-access-gateway', async c => { - const body = await c.req.parseBody(); - const code = typeof body.code === 'string' ? body.code.trim().toUpperCase() : ''; - const userId = typeof body.userId === 'string' ? body.userId.trim() : ''; - - if (!code || !userId) { - return c.html(renderPage({ userId, error: 'Access code and user ID are required.' }), 400); - } - +/** + * Validate an access code, set the auth cookie, and return the redirect URL. + * Returns an error object if validation fails (caller should show an error). + */ +async function redeemCodeAndSetCookie( + c: Context, + code: string, + userId: string +): Promise<{ redirectUrl: string } | { error: string; status: 400 | 401 | 500 }> { const connectionString = c.env.HYPERDRIVE?.connectionString; if (!connectionString) { console.error('[access-gateway] HYPERDRIVE not configured'); - return c.html(renderPage({ userId, error: 'Server configuration error.' }), 500); + return { error: 'Server configuration error.', status: 500 }; } const secret = c.env.NEXTAUTH_SECRET; if (!secret) { console.error('[access-gateway] NEXTAUTH_SECRET not configured'); - return c.html(renderPage({ userId, error: 'Server configuration error.' }), 500); + return { error: 'Server configuration error.', status: 500 }; } const db = getWorkerDb(connectionString); const redeemedUserId = await validateAndRedeemAccessCode(db, code, userId); if (!redeemedUserId) { - return c.html( - renderPage({ - userId, - error: 'Invalid or expired access code. Please generate a new one from your dashboard.', - }), - 401 - ); + return { + error: 'Invalid or expired access code. Please generate a new one from your dashboard.', + status: 401, + }; } - // Look up the user's pepper so the JWT matches what authMiddleware expects const user = await findPepperByUserId(db, redeemedUserId); if (!user) { - return c.html(renderPage({ userId, error: 'User not found.' }), 401); + return { error: 'User not found.', status: 401 }; } const token = await signKiloToken({ @@ -234,7 +211,54 @@ accessGatewayRoutes.post('/kilo-access-gateway', async c => { }); const redirectUrl = await buildRedirectUrl(redeemedUserId, c.env.GATEWAY_TOKEN_SECRET); - return c.html(renderLoadingPage(redirectUrl)); + return { redirectUrl }; +} + +accessGatewayRoutes.get('/kilo-access-gateway', async c => { + const userId = c.req.query('userId'); + if (!userId) { + return c.text('Missing userId parameter', 400); + } + + // If the user already has a valid cookie, derive the gateway token and redirect + const secret = c.env.NEXTAUTH_SECRET; + if (secret) { + const cookie = getCookie(c, KILOCLAW_AUTH_COOKIE); + if (await hasValidCookie(cookie, userId, secret, c.env.WORKER_ENV)) { + const redirectUrl = await buildRedirectUrl(userId, c.env.GATEWAY_TOKEN_SECRET); + return c.redirect(redirectUrl); + } + } + + // If an auth_code is provided in the URL, validate it directly (auto-auth flow). + // This lets the dashboard embed the code in the Open link so users skip manual entry. + const authCode = c.req.query('auth_code')?.trim().toUpperCase(); + if (authCode) { + const result = await redeemCodeAndSetCookie(c, authCode, userId); + if ('redirectUrl' in result) { + return c.html(renderLoadingPage(result.redirectUrl)); + } + // Code was invalid/expired — fall through to the manual form with the error + return c.html(renderPage({ userId, error: result.error }), result.status); + } + + return c.html(renderPage({ userId })); +}); + +accessGatewayRoutes.post('/kilo-access-gateway', async c => { + const body = await c.req.parseBody(); + const code = typeof body.code === 'string' ? body.code.trim().toUpperCase() : ''; + const userId = typeof body.userId === 'string' ? body.userId.trim() : ''; + + if (!code || !userId) { + return c.html(renderPage({ userId, error: 'Access code and user ID are required.' }), 400); + } + + const result = await redeemCodeAndSetCookie(c, code, userId); + if ('redirectUrl' in result) { + return c.html(renderLoadingPage(result.redirectUrl)); + } + return c.html(renderPage({ userId, error: result.error }), result.status); }); export { accessGatewayRoutes }; diff --git a/src/app/(app)/claw/components/AccessCodeActions.tsx b/src/app/(app)/claw/components/AccessCodeActions.tsx index 9f4b6e652..fbf3d6899 100644 --- a/src/app/(app)/claw/components/AccessCodeActions.tsx +++ b/src/app/(app)/claw/components/AccessCodeActions.tsx @@ -1,6 +1,8 @@ 'use client'; -import { Check, Copy, ExternalLink, KeyRound } from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { Check, Copy, ExternalLink, KeyRound, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { useAccessCode } from '../hooks/useAccessCode'; @@ -17,6 +19,24 @@ export function AccessCodeActions({ }) { const { accessCode, isGenerating, isCopied, generateAccessCode, copyAccessCode } = useAccessCode(); + const [isOpening, setIsOpening] = useState(false); + + // Generate a fresh access code and open the gateway URL with it embedded, + // so the user doesn't have to copy-paste the code manually. + const openWithAutoAuth = useCallback(async () => { + setIsOpening(true); + try { + const code = await generateAccessCode(); + if (code) { + const url = `${gatewayUrl}&auth_code=${encodeURIComponent(code)}`; + window.open(url, '_blank', 'noopener,noreferrer'); + } + } catch { + toast.error('Failed to generate access code for auto-login'); + } finally { + setIsOpening(false); + } + }, [gatewayUrl, generateAccessCode]); if (!canShow) return null; @@ -24,7 +44,7 @@ export function AccessCodeActions({ <> - @@ -49,11 +69,18 @@ export function AccessCodeActions({ )} - ); diff --git a/src/app/(app)/claw/hooks/useAccessCode.ts b/src/app/(app)/claw/hooks/useAccessCode.ts index 080895014..afc9a4730 100644 --- a/src/app/(app)/claw/hooks/useAccessCode.ts +++ b/src/app/(app)/claw/hooks/useAccessCode.ts @@ -12,7 +12,7 @@ export function useAccessCode() { const [isGenerating, setIsGenerating] = useState(false); const [isCopied, setIsCopied] = useState(false); - const generateAccessCode = useCallback(async () => { + const generateAccessCode = useCallback(async (): Promise => { setIsGenerating(true); try { const res = await fetch('/api/kiloclaw/access-code', { method: 'POST' }); @@ -20,12 +20,14 @@ export function useAccessCode() { const data = AccessCodeResponse.parse(await res.json()); setAccessCode(data.code); setIsCopied(false); + return data.code; } catch (err) { const message = err instanceof z.ZodError ? 'Unexpected response from access code API' : 'Failed to generate access code'; toast.error(message); + return null; } finally { setIsGenerating(false); } From 32e32c29c5fd592b228f68976e96b1e57bd1a07e Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:56:25 +0000 Subject: [PATCH 2/5] fix: use URL API for safe param appending, remove dead 400 from union Address PR feedback: - Use URL + searchParams.set() instead of string concatenation to safely append auth_code, avoiding breakage if gatewayUrl has no existing query string. - Remove 400 from redeemCodeAndSetCookie return type since no code path returns it. --- kiloclaw/src/routes/access-gateway.ts | 2 +- src/app/(app)/claw/components/AccessCodeActions.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/kiloclaw/src/routes/access-gateway.ts b/kiloclaw/src/routes/access-gateway.ts index a3e9fc23d..afb24e0b2 100644 --- a/kiloclaw/src/routes/access-gateway.ts +++ b/kiloclaw/src/routes/access-gateway.ts @@ -167,7 +167,7 @@ async function redeemCodeAndSetCookie( c: Context, code: string, userId: string -): Promise<{ redirectUrl: string } | { error: string; status: 400 | 401 | 500 }> { +): Promise<{ redirectUrl: string } | { error: string; status: 401 | 500 }> { const connectionString = c.env.HYPERDRIVE?.connectionString; if (!connectionString) { console.error('[access-gateway] HYPERDRIVE not configured'); diff --git a/src/app/(app)/claw/components/AccessCodeActions.tsx b/src/app/(app)/claw/components/AccessCodeActions.tsx index fbf3d6899..f3502adca 100644 --- a/src/app/(app)/claw/components/AccessCodeActions.tsx +++ b/src/app/(app)/claw/components/AccessCodeActions.tsx @@ -28,8 +28,9 @@ export function AccessCodeActions({ try { const code = await generateAccessCode(); if (code) { - const url = `${gatewayUrl}&auth_code=${encodeURIComponent(code)}`; - window.open(url, '_blank', 'noopener,noreferrer'); + const url = new URL(gatewayUrl, window.location.origin); + url.searchParams.set('auth_code', code); + window.open(url.toString(), '_blank', 'noopener,noreferrer'); } } catch { toast.error('Failed to generate access code for auto-login'); From 7296c4a77e6cafd53242f248ac3e5c906a7eacdf Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:01:47 +0000 Subject: [PATCH 3/5] fix: handle popup blocker and prevent concurrent code generation - Check window.open return value and toast an error if the popup was blocked, so the user knows the code was wasted. - Disable the Open button while isGenerating to prevent racing with an in-flight Access Code generation. --- src/app/(app)/claw/components/AccessCodeActions.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/claw/components/AccessCodeActions.tsx b/src/app/(app)/claw/components/AccessCodeActions.tsx index f3502adca..b134b8905 100644 --- a/src/app/(app)/claw/components/AccessCodeActions.tsx +++ b/src/app/(app)/claw/components/AccessCodeActions.tsx @@ -30,7 +30,10 @@ export function AccessCodeActions({ if (code) { const url = new URL(gatewayUrl, window.location.origin); url.searchParams.set('auth_code', code); - window.open(url.toString(), '_blank', 'noopener,noreferrer'); + const win = window.open(url.toString(), '_blank', 'noopener,noreferrer'); + if (!win) { + toast.error('Popup blocked — please allow popups and try again'); + } } } catch { toast.error('Failed to generate access code for auto-login'); @@ -73,7 +76,7 @@ export function AccessCodeActions({ - - {accessCode && ( - -
-

One-time code (expires in 10 min)

-
- - {accessCode} - - -
-
-
- )} - - - - ); -} diff --git a/src/app/(app)/claw/components/ClawHeader.tsx b/src/app/(app)/claw/components/ClawHeader.tsx index 008af18a0..d64e34b7c 100644 --- a/src/app/(app)/claw/components/ClawHeader.tsx +++ b/src/app/(app)/claw/components/ClawHeader.tsx @@ -2,7 +2,7 @@ import { Badge } from '@/components/ui/badge'; import KiloCrabIcon from '@/components/KiloCrabIcon'; -import { AccessCodeActions } from './AccessCodeActions'; +import { OpenClawButton } from './OpenClawButton'; import { CLAW_STATUS_BADGE, type ClawState } from './claw.types'; export function ClawHeader({ @@ -43,10 +43,7 @@ export function ClawHeader({
- +
); diff --git a/src/app/(app)/claw/components/OpenClawButton.tsx b/src/app/(app)/claw/components/OpenClawButton.tsx new file mode 100644 index 000000000..174fe15d1 --- /dev/null +++ b/src/app/(app)/claw/components/OpenClawButton.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useCallback, useState } from 'react'; +import { ExternalLink, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { useAccessCode } from '../hooks/useAccessCode'; + +const OPEN_BUTTON_ACCENT_CLASS = + 'animate-pulse-once bg-[oklch(95%_0.15_108)] text-black shadow-[0_0_20px_rgba(237,255,0,0.3)] ring-[oklch(95%_0.15_108)]/20 transition-all duration-500 ease-in-out hover:bg-[oklch(95%_0.15_108)]/90 hover:ring-[oklch(95%_0.15_108)]/40'; + +export function OpenClawButton({ canShow, gatewayUrl }: { canShow: boolean; gatewayUrl: string }) { + const { isGenerating, generateAccessCode } = useAccessCode(); + const [isOpening, setIsOpening] = useState(false); + + // Open the window synchronously (in the click handler's call stack) to avoid + // popup blockers, then navigate it once the access code arrives. + const openWithAutoAuth = useCallback(async () => { + setIsOpening(true); + const win = window.open('about:blank', '_blank'); + try { + const code = await generateAccessCode(); + if (code && win) { + const url = new URL(gatewayUrl, window.location.origin); + url.searchParams.set('auth_code', code); + win.location.href = url.toString(); + } else { + win?.close(); + } + } catch { + win?.close(); + toast.error('Failed to generate access code for auto-login'); + } finally { + setIsOpening(false); + } + }, [gatewayUrl, generateAccessCode]); + + if (!canShow) return null; + + return ( + + ); +} diff --git a/src/app/(app)/claw/components/index.ts b/src/app/(app)/claw/components/index.ts index 3e6491542..a9872f18f 100644 --- a/src/app/(app)/claw/components/index.ts +++ b/src/app/(app)/claw/components/index.ts @@ -1,4 +1,4 @@ -export { AccessCodeActions } from './AccessCodeActions'; +export { OpenClawButton } from './OpenClawButton'; export { ClawDashboard } from './ClawDashboard'; export { ClawHeader } from './ClawHeader'; export { CreateInstanceCard } from './CreateInstanceCard'; diff --git a/src/app/(app)/claw/hooks/useAccessCode.ts b/src/app/(app)/claw/hooks/useAccessCode.ts index afc9a4730..8dfbe65e0 100644 --- a/src/app/(app)/claw/hooks/useAccessCode.ts +++ b/src/app/(app)/claw/hooks/useAccessCode.ts @@ -8,9 +8,7 @@ const AccessCodeResponse = z.object({ }); export function useAccessCode() { - const [accessCode, setAccessCode] = useState(null); const [isGenerating, setIsGenerating] = useState(false); - const [isCopied, setIsCopied] = useState(false); const generateAccessCode = useCallback(async (): Promise => { setIsGenerating(true); @@ -18,8 +16,6 @@ export function useAccessCode() { const res = await fetch('/api/kiloclaw/access-code', { method: 'POST' }); if (!res.ok) throw new Error('Failed to generate access code'); const data = AccessCodeResponse.parse(await res.json()); - setAccessCode(data.code); - setIsCopied(false); return data.code; } catch (err) { const message = @@ -33,30 +29,8 @@ export function useAccessCode() { } }, []); - const copyAccessCode = useCallback(async () => { - if (!accessCode) return; - if (typeof navigator === 'undefined' || typeof navigator.clipboard?.writeText !== 'function') { - setIsCopied(false); - toast.error('Clipboard is not available in this environment'); - return; - } - - try { - await navigator.clipboard.writeText(accessCode); - setIsCopied(true); - toast.success('Access code copied'); - setTimeout(() => setIsCopied(false), 2000); - } catch { - setIsCopied(false); - toast.error('Failed to copy access code'); - } - }, [accessCode]); - return { - accessCode, isGenerating, - isCopied, generateAccessCode, - copyAccessCode, }; } From d023ceb198056a7c83cf23dd0b18371693a01672 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 3 Mar 2026 10:41:57 -0600 Subject: [PATCH 5/5] Update src/app/(app)/claw/components/OpenClawButton.tsx Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> --- src/app/(app)/claw/components/OpenClawButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(app)/claw/components/OpenClawButton.tsx b/src/app/(app)/claw/components/OpenClawButton.tsx index 174fe15d1..e1bb362e4 100644 --- a/src/app/(app)/claw/components/OpenClawButton.tsx +++ b/src/app/(app)/claw/components/OpenClawButton.tsx @@ -29,7 +29,7 @@ export function OpenClawButton({ canShow, gatewayUrl }: { canShow: boolean; gate } } catch { win?.close(); - toast.error('Failed to generate access code for auto-login'); + toast.error('Failed to open KiloClaw — invalid gateway URL'); } finally { setIsOpening(false); }