From 3df466d7abefaffc731839fb7de0086f51633299 Mon Sep 17 00:00:00 2001 From: j4ckxyz Date: Sun, 19 Apr 2026 20:59:04 +0100 Subject: [PATCH] Show friendly OAuth completion page Return a small local HTML confirmation after OAuth callback with the authenticated handle, while preserving callback validation and no-store caching. --- src/oauth.ts | 76 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/oauth.ts b/src/oauth.ts index 8310eaf..b699bbf 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -129,6 +129,68 @@ function buildLoopbackClientId(redirectUri: string, scope: string): string { return `${LOOPBACK_ORIGIN}?${params.toString()}`; } +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + +function buildOAuthCallbackHtml(handle: string, ok: boolean): string { + const safeHandle = escapeHtml(handle); + const heading = ok + ? `🎉 You have authenticated as ${safeHandle}.` + : `Authentication did not complete for ${safeHandle}.`; + const message = ok + ? "You can close this page now and return to the terminal." + : "You can close this page and return to the terminal for details."; + + return ` + + + + + grain login + + + +
+

${heading}

+

${message}

+
+ +`; +} + function createClient(clientId: string, redirectUri: string, scope: string): NodeOAuthClient { const stores = makeStores(); @@ -149,7 +211,7 @@ function createClient(clientId: string, redirectUri: string, scope: string): Nod }); } -async function waitForOAuthCallback(redirectPort: number, timeoutMs = 180000): Promise { +async function waitForOAuthCallback(handle: string, redirectPort: number, timeoutMs = 180000): Promise { let resolveResult!: (params: URLSearchParams) => void; let rejectResult!: (error: Error) => void; @@ -172,8 +234,16 @@ async function waitForOAuthCallback(redirectPort: number, timeoutMs = 180000): P return new Response("Not found", { status: 404 }); } + const hasError = url.searchParams.has("error"); + resolveResult(url.searchParams); - return new Response("Login complete. Return to terminal.", { status: 200 }); + return new Response(buildOAuthCallbackHtml(handle, !hasError), { + status: hasError ? 400 : 200, + headers: { + "content-type": "text/html; charset=utf-8", + "cache-control": "no-store", + }, + }); } catch (error) { rejectResult(new GrainError("oauth_callback_error", error instanceof Error ? error.message : String(error))); return new Response("OAuth callback failed.", { status: 500 }); @@ -251,7 +321,7 @@ export async function loginWithOAuth(handle: string): Promise state: appState, }); - const callbackPromise = waitForOAuthCallback(redirectPort); + const callbackPromise = waitForOAuthCallback(handle, redirectPort); console.log("Open the following URL to continue login:"); console.log(authorizeUrl.toString()); -- knotmirror 0.0.1