diff --git a/src/server.ts b/src/server.ts index 7dd9d99..1767f53 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,51 +5,42 @@ * JWKS, discovery) are served by its node handler. We add a tiny login page * (the OIDC `loginPage` target) with Google/Apple buttons, and a health check. */ -import { createServer } from "node:http"; +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { toNodeHandler } from "better-auth/node"; import { auth } from "./auth.js"; const PORT = Number(process.env.PORT ?? 8080); const handler = toNodeHandler(auth); -function loginPage(rawSearch: string): string { - // Better Auth's oauth-provider redirects here with the original, signed - // authorize params in the query. After sign-in we must hand them back to the - // authorize endpoint verbatim so it resumes and issues the code. If there's no - // OAuth context (a stray visit), fall back to the portal. - const cb = rawSearch ? "/api/auth/oauth2/authorize" + rawSearch : "/portal"; - const social = (provider: string, label: string) => ` - `; - return ` - - -Sign in — Intervals.icu MCP - -

Sign in to Intervals.icu MCP

-

Connect your Intervals.icu account to use it from Claude.

-${auth.options.socialProviders && "google" in auth.options.socialProviders ? social("google", "Continue with Google") : ""} -${auth.options.socialProviders && "apple" in auth.options.socialProviders ? social("apple", "Continue with Apple") : ""} - -`; +// Better Auth's oauth-provider redirects here (its loginPage) with the original +// signed authorize params in the query. Rather than serve an interactive HTML +// page (Claude's OAuth window doesn't run our JS), initiate Google sign-in +// server-side and 302 straight to Google. After Google, Better Auth returns to +// the authorize endpoint (via callbackURL) and resumes — a pure redirect chain +// that any OAuth-following client handles. Google is the only provider for now. +async function startLogin(req: IncomingMessage, res: ServerResponse, rawSearch: string) { + const callbackURL = rawSearch ? "/api/auth/oauth2/authorize" + rawSearch : "/portal"; + try { + const { headers, response } = await auth.api.signInSocial({ + body: { provider: "google", callbackURL }, + returnHeaders: true, + }); + const setCookie = headers.getSetCookie(); + if (setCookie.length) res.setHeader("set-cookie", setCookie); + const target = (response as { url?: string } | null)?.url; + if (!target) { + res.writeHead(500, { "content-type": "text/plain" }); + res.end("Could not start sign-in."); + return; + } + res.writeHead(302, { location: target }); + res.end(); + } catch (err) { + // eslint-disable-next-line no-console + console.error("[login] signInSocial failed", err); + res.writeHead(500, { "content-type": "text/plain" }); + res.end("Sign-in error."); + } } const server = createServer((req, res) => { @@ -66,8 +57,7 @@ const server = createServer((req, res) => { return; } if (url.pathname === "/login" && req.method === "GET") { - res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); - res.end(loginPage(url.search)); + void startLogin(req, res, url.search); return; } // Everything else -> Better Auth (async handler).