From aca1edcc8d30de56123089feb3916b90ea737da5 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Mon, 11 May 2026 14:47:42 +0800 Subject: [PATCH] Remove Vercel OAuth login --- AGENTS.md | 6 +- Dockerfile | 5 - README.md | 3 +- app/api/auth/callback/vercel/route.ts | 73 -- app/api/auth/github/callback/route.ts | 165 +---- app/api/auth/github/disconnect/route.ts | 39 -- app/api/auth/github/signin/route.ts | 128 ---- app/api/auth/github/status/route.ts | 22 +- app/api/auth/info/route.ts | 37 +- app/api/auth/signin/vercel/route.ts | 46 -- app/api/auth/signout/route.ts | 51 +- app/api/vercel/teams/route.ts | 49 -- app/repos/[owner]/[repo]/layout.tsx | 10 +- app/tasks/[taskId]/page.tsx | 10 +- app/tasks/page.tsx | 2 +- components/auth/session-provider.tsx | 8 +- components/auth/sign-in.tsx | 140 ++-- components/auth/sign-out.tsx | 79 +-- components/auth/user.tsx | 8 +- components/repo-layout.tsx | 5 +- components/repo-page-client.tsx | 5 +- components/repo-selector.tsx | 58 +- components/sealos-home-page-content.tsx | 136 +--- components/sealos-task-page-client.tsx | 3 - components/task-form.tsx | 2 +- components/task-page-client.tsx | 11 +- components/task-sidebar.tsx | 2 +- components/tasks-list-client.tsx | 5 +- lib/auth/account-merge.ts | 36 - lib/auth/iframe-oauth.test.ts | 15 - lib/auth/oauth-removal-regression.test.ts | 53 ++ lib/auth/providers.ts | 16 - lib/db/schema.ts | 6 +- lib/db/users.ts | 43 +- lib/session/create.ts | 72 -- lib/session/get-oauth-token.ts | 90 +-- lib/session/redirect-to-sign-in.ts | 14 - lib/session/server.ts | 22 +- lib/session/types.ts | 10 +- lib/vercel-client/projects.ts | 75 -- lib/vercel-client/teams.ts | 25 - lib/vercel-client/types.ts | 17 - lib/vercel-client/user.ts | 35 - package.json | 1 - plan.md | 795 ++++++++++++++++++++++ pnpm-lock.yaml | 519 +------------- reference/configuration.md | 3 +- 47 files changed, 1076 insertions(+), 1879 deletions(-) delete mode 100644 app/api/auth/callback/vercel/route.ts delete mode 100644 app/api/auth/github/disconnect/route.ts delete mode 100644 app/api/auth/github/signin/route.ts delete mode 100644 app/api/auth/signin/vercel/route.ts delete mode 100644 app/api/vercel/teams/route.ts delete mode 100644 lib/auth/account-merge.ts create mode 100644 lib/auth/oauth-removal-regression.test.ts delete mode 100644 lib/auth/providers.ts delete mode 100644 lib/session/create.ts delete mode 100644 lib/session/redirect-to-sign-in.ts delete mode 100644 lib/vercel-client/projects.ts delete mode 100644 lib/vercel-client/teams.ts delete mode 100644 lib/vercel-client/types.ts delete mode 100644 lib/vercel-client/user.ts create mode 100644 plan.md diff --git a/AGENTS.md b/AGENTS.md index e624118..47ffadc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -238,15 +238,13 @@ Never expose these in logs or to the client: ### Client-Safe Variables -Only these variables should be exposed to the client (via `NEXT_PUBLIC_` prefix): - -- `NEXT_PUBLIC_AUTH_PROVIDERS` - Available auth providers +The current app does not require any client-safe auth provider variables. Do not add `NEXT_PUBLIC_` variables unless the app code explicitly needs them. ### Runtime Variables Used By Current App Code The current codebase expects these environment variables: -- Required core runtime: `POSTGRES_URL`, `SEALOS_HOST`, `DEVBOX_TOKEN`, `JWE_SECRET`, `ENCRYPTION_KEY`, `NEXT_PUBLIC_AUTH_PROVIDERS`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` +- Required core runtime: `POSTGRES_URL`, `SEALOS_HOST`, `DEVBOX_TOKEN`, `JWE_SECRET`, `ENCRYPTION_KEY`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` - Required for Devbox JWT auth when `DEVBOX_TOKEN` is not set: `DEVBOX_JWT_SIGNING_KEY` - Required for AI-generated branch names, titles, and commit messages: `AI_GATEWAY_API_KEY` - Optional callback override for self-hosted deployments: `APP_BASE_URL` diff --git a/Dockerfile b/Dockerfile index ed43f59..c277987 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,14 +23,9 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -ARG NEXT_PUBLIC_AUTH_PROVIDERS=github -ARG NEXT_PUBLIC_VERCEL_CLIENT_ID="" - ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_OPTIONS=--max-old-space-size=4096 -ENV NEXT_PUBLIC_AUTH_PROVIDERS=$NEXT_PUBLIC_AUTH_PROVIDERS -ENV NEXT_PUBLIC_VERCEL_CLIENT_ID=$NEXT_PUBLIC_VERCEL_CLIENT_ID RUN pnpm build diff --git a/README.md b/README.md index 931fe96..8fb8653 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ SEALOS_HOST= DEVBOX_TOKEN= JWE_SECRET= ENCRYPTION_KEY= -NEXT_PUBLIC_AUTH_PROVIDERS=github GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= AI_GATEWAY_API_KEY= @@ -137,7 +136,7 @@ pnpm db:studio ## Configuration Notes - The current task execution path is intentionally pinned to `codex` + `gpt-5.4`. -- `NEXT_PUBLIC_AUTH_PROVIDERS` is expected to include `github`. +- Authentication is GitHub OAuth-only; configure `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`. - Users can provide their own API keys in the app, which can override global key configuration. - Connectors are managed from the application UI; if a connector stores OAuth credentials, `ENCRYPTION_KEY` must be set. diff --git a/app/api/auth/callback/vercel/route.ts b/app/api/auth/callback/vercel/route.ts deleted file mode 100644 index ee7d4af..0000000 --- a/app/api/auth/callback/vercel/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { type NextRequest } from 'next/server' -import { OAuth2Client, type OAuth2Tokens } from 'arctic' -import { getAppBaseUrl } from '@/lib/auth/oauth' -import { createSession, saveSession } from '@/lib/session/create' -import { cookies } from 'next/headers' -import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy' - -export async function GET(req: NextRequest): Promise { - const code = req.nextUrl.searchParams.get('code') - const state = req.nextUrl.searchParams.get('state') - const cookieStore = await cookies() - const authCookiePolicy = getAuthCookiePolicyFromRequest(req) - const storedState = cookieStore.get(`vercel_oauth_state`)?.value ?? null - const storedVerifier = cookieStore.get(`vercel_oauth_code_verifier`)?.value ?? null - const storedRedirectTo = cookieStore.get(`vercel_oauth_redirect_to`)?.value ?? null - - if ( - code === null || - state === null || - storedState !== state || - storedRedirectTo === null || - storedVerifier === null - ) { - return new Response(null, { - status: 400, - }) - } - - const client = new OAuth2Client( - process.env.NEXT_PUBLIC_VERCEL_CLIENT_ID ?? '', - process.env.VERCEL_CLIENT_SECRET ?? '', - `${getAppBaseUrl(req)}/api/auth/callback/vercel`, - ) - - let tokens: OAuth2Tokens - - try { - tokens = await client.validateAuthorizationCode('https://vercel.com/api/login/oauth/token', code, storedVerifier) - } catch { - console.error('Failed to validate authorization code') - return new Response(null, { - status: 400, - }) - } - - const response = new Response(null, { - status: 302, - headers: { - Location: storedRedirectTo, - }, - }) - - const session = await createSession({ - accessToken: tokens.accessToken(), - expiresAt: tokens.accessTokenExpiresAt().getTime(), - refreshToken: tokens.hasRefreshToken() ? tokens.refreshToken() : undefined, - }) - - if (!session) { - console.error('[Vercel Callback] Failed to create session') - return new Response('Failed to create session', { status: 500 }) - } - - // Note: Vercel tokens are already stored in users table by upsertUser() in createSession() - - await saveSession(response, session, authCookiePolicy) - - cookieStore.delete(`vercel_oauth_state`) - cookieStore.delete(`vercel_oauth_code_verifier`) - cookieStore.delete(`vercel_oauth_redirect_to`) - - return response -} diff --git a/app/api/auth/github/callback/route.ts b/app/api/auth/github/callback/route.ts index 51560a5..fb222e7 100644 --- a/app/api/auth/github/callback/route.ts +++ b/app/api/auth/github/callback/route.ts @@ -1,13 +1,7 @@ import { type NextRequest } from 'next/server' import { cookies } from 'next/headers' -import { db } from '@/lib/db/client' -import { users, accounts, tasks, connectors, keys } from '@/lib/db/schema' -import { eq, and, inArray } from 'drizzle-orm' import { getAppBaseUrl, getGitHubClientId } from '@/lib/auth/oauth' import { createGitHubSession, saveSession } from '@/lib/session/create-github' -import { encrypt } from '@/lib/crypto' -import { generateId } from '@/lib/utils/id' -import { planUserKeyMerge } from '@/lib/auth/account-merge' import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy' import { GITHUB_AUTH_BROADCAST_CHANNEL, @@ -22,7 +16,6 @@ const GITHUB_AUTH_COOKIES = [ 'github_auth_state', 'github_auth_redirect_to', 'github_auth_mode', - 'github_auth_user_id', 'github_oauth_state', 'github_oauth_redirect_to', 'github_oauth_user_id', @@ -102,18 +95,15 @@ export async function GET(req: NextRequest): Promise { } const authMode = cookieStore.get('github_auth_mode')?.value ?? null - const isSignInFlow = authMode === 'signin' - const isConnectFlow = authMode === 'connect' const storedState = cookieStore.get('github_auth_state')?.value ?? null const storedRedirectTo = cookieStore.get('github_auth_redirect_to')?.value ?? null - const storedUserId = cookieStore.get('github_auth_user_id')?.value ?? null if (code === null || state === null || storedState !== state || storedRedirectTo === null) { cleanupGitHubAuthCookies(cookieStore) return createGitHubPopupResponse(req, 'error', { status: 400 }) } - if ((!isSignInFlow && !isConnectFlow) || (isConnectFlow && storedUserId === null)) { + if (authMode !== 'signin') { cleanupGitHubAuthCookies(cookieStore) return createGitHubPopupResponse(req, 'error', { status: 400 }) } @@ -162,156 +152,19 @@ export async function GET(req: NextRequest): Promise { return createGitHubPopupResponse(req, 'error', { status: 400 }) } - const userResponse = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${tokenData.access_token}`, - Accept: 'application/vnd.github.v3+json', - }, - }) + const session = await createGitHubSession(tokenData.access_token, tokenData.scope) - if (!userResponse.ok) { - console.error('GitHub OAuth user fetch failed') + if (!session) { + console.error('GitHub OAuth session creation failed') cleanupGitHubAuthCookies(cookieStore) - return createGitHubPopupResponse(req, 'error', { status: 400 }) - } - - const githubUser = (await userResponse.json()) as { - login: string - id: number - } - - if (isSignInFlow) { - const session = await createGitHubSession(tokenData.access_token, tokenData.scope) - - if (!session) { - console.error('GitHub OAuth session creation failed') - cleanupGitHubAuthCookies(cookieStore) - return createGitHubPopupResponse(req, 'error', { status: 500 }) - } - - const response = createGitHubPopupResponse(req, 'success') - await saveSession(response, session, authCookiePolicy) - cleanupGitHubAuthCookies(cookieStore) - - return response - } - - const encryptedToken = encrypt(tokenData.access_token) - const targetUserId = storedUserId! - - const existingAccount = await db - .select() - .from(accounts) - .where(and(eq(accounts.provider, 'github'), eq(accounts.externalUserId, `${githubUser.id}`))) - .limit(1) - - if (existingAccount.length > 0) { - const connectedUserId = existingAccount[0].userId - - if (connectedUserId !== targetUserId) { - console.info('GitHub OAuth account merge started') - - await db.transaction(async (tx) => { - await tx.update(tasks).set({ userId: targetUserId }).where(eq(tasks.userId, connectedUserId)) - await tx.update(connectors).set({ userId: targetUserId }).where(eq(connectors.userId, connectedUserId)) - - const [targetAccount] = await tx - .select({ id: accounts.id }) - .from(accounts) - .where(and(eq(accounts.userId, targetUserId), eq(accounts.provider, 'github'))) - .limit(1) - - const sourceKeys = await tx - .select({ id: keys.id, provider: keys.provider }) - .from(keys) - .where(eq(keys.userId, connectedUserId)) - const targetKeys = await tx - .select({ id: keys.id, provider: keys.provider }) - .from(keys) - .where(eq(keys.userId, targetUserId)) - const keyMergePlan = planUserKeyMerge({ sourceKeys, targetKeys }) - - if (keyMergePlan.moveKeyIds.length > 0) { - await tx.update(keys).set({ userId: targetUserId }).where(inArray(keys.id, keyMergePlan.moveKeyIds)) - } - - if (keyMergePlan.deleteKeyIds.length > 0) { - await tx.delete(keys).where(inArray(keys.id, keyMergePlan.deleteKeyIds)) - } - - if (targetAccount) { - await tx - .update(accounts) - .set({ - accessToken: encryptedToken, - externalUserId: `${githubUser.id}`, - scope: tokenData.scope, - username: githubUser.login, - updatedAt: new Date(), - }) - .where(eq(accounts.id, targetAccount.id)) - await tx.delete(accounts).where(eq(accounts.id, existingAccount[0].id)) - } else { - await tx - .update(accounts) - .set({ - userId: targetUserId, - accessToken: encryptedToken, - scope: tokenData.scope, - username: githubUser.login, - updatedAt: new Date(), - }) - .where(eq(accounts.id, existingAccount[0].id)) - } - - await tx.delete(users).where(eq(users.id, connectedUserId)) - }) - - console.info('GitHub OAuth account merge completed') - } else { - await db - .update(accounts) - .set({ - accessToken: encryptedToken, - scope: tokenData.scope, - username: githubUser.login, - updatedAt: new Date(), - }) - .where(eq(accounts.id, existingAccount[0].id)) - } - } else { - const [currentAccount] = await db - .select({ id: accounts.id }) - .from(accounts) - .where(and(eq(accounts.userId, targetUserId), eq(accounts.provider, 'github'))) - .limit(1) - - if (currentAccount) { - await db - .update(accounts) - .set({ - externalUserId: `${githubUser.id}`, - accessToken: encryptedToken, - scope: tokenData.scope, - username: githubUser.login, - updatedAt: new Date(), - }) - .where(eq(accounts.id, currentAccount.id)) - } else { - await db.insert(accounts).values({ - id: generateId(21), - userId: targetUserId, - provider: 'github', - externalUserId: `${githubUser.id}`, - accessToken: encryptedToken, - scope: tokenData.scope, - username: githubUser.login, - }) - } + return createGitHubPopupResponse(req, 'error', { status: 500 }) } + const response = createGitHubPopupResponse(req, 'success') + await saveSession(response, session, authCookiePolicy) cleanupGitHubAuthCookies(cookieStore) - return createGitHubPopupResponse(req, 'success') + + return response } catch { console.error('GitHub OAuth callback failed') cleanupGitHubAuthCookies(cookieStore) diff --git a/app/api/auth/github/disconnect/route.ts b/app/api/auth/github/disconnect/route.ts deleted file mode 100644 index f017ddb..0000000 --- a/app/api/auth/github/disconnect/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { type NextRequest } from 'next/server' -import { getSessionFromReq } from '@/lib/session/server' -import { db } from '@/lib/db/client' -import { accounts } from '@/lib/db/schema' -import { eq, and } from 'drizzle-orm' - -export async function POST(req: NextRequest) { - const session = await getSessionFromReq(req) - - if (!session?.user) { - console.log('Disconnect GitHub: No session found') - return Response.json({ error: 'Not authenticated' }, { status: 401 }) - } - - if (!session.user.id) { - console.error('Session user.id is undefined. Session:', session) - return Response.json({ error: 'Invalid session - user ID missing' }, { status: 400 }) - } - - // Can only disconnect if user didn't sign in with GitHub - if (session.authProvider === 'github') { - return Response.json({ error: 'Cannot disconnect primary authentication method' }, { status: 400 }) - } - - console.log('Disconnecting GitHub account for user:', session.user.id) - - try { - await db.delete(accounts).where(and(eq(accounts.userId, session.user.id), eq(accounts.provider, 'github'))) - - console.log('GitHub account disconnected successfully for user:', session.user.id) - return Response.json({ success: true }) - } catch (error) { - console.error('Error disconnecting GitHub:', error) - return Response.json( - { error: 'Failed to disconnect', details: error instanceof Error ? error.message : 'Unknown error' }, - { status: 500 }, - ) - } -} diff --git a/app/api/auth/github/signin/route.ts b/app/api/auth/github/signin/route.ts deleted file mode 100644 index 8103bf4..0000000 --- a/app/api/auth/github/signin/route.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { type NextRequest } from 'next/server' -import { cookies } from 'next/headers' -import { getSessionFromReq } from '@/lib/session/server' -import { GITHUB_OAUTH_SCOPE, getAppBaseUrl, getGitHubClientId } from '@/lib/auth/oauth' -import { isRelativeUrl } from '@/lib/utils/is-relative-url' -import { generateState } from 'arctic' -import { getAuthCookiePolicyFromRequest, getAuthCookieSameSite, getAuthCookieSecure } from '@/lib/auth/cookie-policy' -import { - GITHUB_AUTH_POPUP_COOKIE, - GITHUB_AUTH_POPUP_PARAM, - GITHUB_AUTH_POPUP_VALUE, -} from '@/lib/auth/github-popup-contract' - -const GITHUB_AUTH_COOKIE_MAX_AGE = 60 * 10 - -function setGitHubAuthCookie( - store: Awaited>, - key: string, - value: string, - authCookiePolicy: ReturnType, -): void { - store.set(key, value, { - path: '/', - secure: getAuthCookieSecure(authCookiePolicy), - httpOnly: true, - maxAge: GITHUB_AUTH_COOKIE_MAX_AGE, - sameSite: getAuthCookieSameSite(authCookiePolicy), - }) -} - -export async function GET(req: NextRequest): Promise { - if (req.nextUrl.searchParams.get(GITHUB_AUTH_POPUP_PARAM) !== GITHUB_AUTH_POPUP_VALUE) { - return new Response('Invalid GitHub authentication request', { status: 400 }) - } - - // Check if user is authenticated with Vercel first - const session = await getSessionFromReq(req) - if (!session?.user) { - return Response.redirect(new URL('/', getAppBaseUrl(req))) - } - - const clientId = getGitHubClientId() - const redirectUri = `${getAppBaseUrl(req)}/api/auth/github/callback` - - if (!clientId) { - return Response.redirect(new URL('/?error=github_not_configured', getAppBaseUrl(req))) - } - - const state = generateState() - const store = await cookies() - const authCookiePolicy = getAuthCookiePolicyFromRequest(req) - const redirectTo = isRelativeUrl(req.nextUrl.searchParams.get('next') ?? '/') - ? (req.nextUrl.searchParams.get('next') ?? '/') - : '/' - - // Store state and redirect URL - for (const [key, value] of [ - [GITHUB_AUTH_POPUP_COOKIE, GITHUB_AUTH_POPUP_VALUE], - ['github_auth_redirect_to', redirectTo], - ['github_auth_state', state], - ['github_auth_mode', 'connect'], - ['github_auth_user_id', session.user.id], - ]) { - setGitHubAuthCookie(store, key, value, authCookiePolicy) - } - - // Build GitHub authorization URL - const params = new URLSearchParams({ - client_id: clientId, - redirect_uri: redirectUri, - scope: GITHUB_OAUTH_SCOPE, - state: state, - }) - - const url = `https://github.com/login/oauth/authorize?${params.toString()}` - - // Redirect directly to GitHub - return Response.redirect(url) -} - -export async function POST(req: NextRequest): Promise { - if (req.nextUrl.searchParams.get(GITHUB_AUTH_POPUP_PARAM) !== GITHUB_AUTH_POPUP_VALUE) { - return Response.json({ error: 'Invalid GitHub authentication request' }, { status: 400 }) - } - - // Check if user is authenticated with Vercel first - const session = await getSessionFromReq(req) - if (!session?.user) { - return Response.json({ error: 'Not authenticated' }, { status: 401 }) - } - - const clientId = getGitHubClientId() - const redirectUri = `${getAppBaseUrl(req)}/api/auth/github/callback` - - if (!clientId) { - return Response.json({ error: 'GitHub OAuth not configured' }, { status: 500 }) - } - - const state = generateState() - const store = await cookies() - const authCookiePolicy = getAuthCookiePolicyFromRequest(req) - const redirectTo = isRelativeUrl(req.nextUrl.searchParams.get('next') ?? '/') - ? (req.nextUrl.searchParams.get('next') ?? '/') - : '/' - - // Store state and redirect URL - for (const [key, value] of [ - [GITHUB_AUTH_POPUP_COOKIE, GITHUB_AUTH_POPUP_VALUE], - ['github_auth_redirect_to', redirectTo], - ['github_auth_state', state], - ['github_auth_mode', 'connect'], - ['github_auth_user_id', session.user.id], - ]) { - setGitHubAuthCookie(store, key, value, authCookiePolicy) - } - - // Build GitHub authorization URL - const params = new URLSearchParams({ - client_id: clientId, - redirect_uri: redirectUri, - scope: GITHUB_OAUTH_SCOPE, - state: state, - }) - - const url = `https://github.com/login/oauth/authorize?${params.toString()}` - - return Response.json({ url }) -} diff --git a/app/api/auth/github/status/route.ts b/app/api/auth/github/status/route.ts index 698115d..fa36ce8 100644 --- a/app/api/auth/github/status/route.ts +++ b/app/api/auth/github/status/route.ts @@ -3,6 +3,7 @@ import { getSessionFromReq } from '@/lib/session/server' import { db } from '@/lib/db/client' import { users, accounts } from '@/lib/db/schema' import { eq, and } from 'drizzle-orm' +import { getOAuthToken } from '@/lib/session/get-oauth-token' export async function GET(req: NextRequest) { const session = await getSessionFromReq(req) @@ -17,6 +18,23 @@ export async function GET(req: NextRequest) { } try { + const tokenData = await getOAuthToken(session.user.id, 'github') + if (!tokenData) { + return Response.json({ connected: false }) + } + + const githubResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${tokenData.accessToken}`, + Accept: 'application/vnd.github.v3+json', + }, + cache: 'no-store', + }) + + if (!githubResponse.ok) { + return Response.json({ connected: false }) + } + // Check if user has GitHub as connected account const account = await db .select({ @@ -54,8 +72,8 @@ export async function GET(req: NextRequest) { } return Response.json({ connected: false }) - } catch (error) { - console.error('Error checking GitHub connection status:', error) + } catch { + console.error('Error checking GitHub connection status') return Response.json({ connected: false, error: 'Failed to check status' }, { status: 500 }) } } diff --git a/app/api/auth/info/route.ts b/app/api/auth/info/route.ts index c163db6..215175a 100644 --- a/app/api/auth/info/route.ts +++ b/app/api/auth/info/route.ts @@ -1,47 +1,18 @@ import type { NextRequest } from 'next/server' -import type { Session, SessionUserInfo, Tokens } from '@/lib/session/types' -import { createSession, saveSession } from '@/lib/session/create' -import { saveSession as saveGitHubSession } from '@/lib/session/create-github' +import type { Session, SessionUserInfo } from '@/lib/session/types' +import { saveSession } from '@/lib/session/create-github' import { getSessionFromReq } from '@/lib/session/server' -import { getOAuthToken } from '@/lib/session/get-oauth-token' import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy' export async function GET(req: NextRequest) { - const existingSession = await getSessionFromReq(req) + const session = await getSessionFromReq(req) const authCookiePolicy = getAuthCookiePolicyFromRequest(req) - // For GitHub users, just return the existing session without recreating it - // For Vercel users, recreate the session to refresh user data - let session: Session | undefined - if (existingSession && existingSession.authProvider === 'github') { - session = existingSession - } else if (existingSession) { - // Fetch Vercel token from database to recreate session - const tokenData = await getOAuthToken(existingSession.user.id, 'vercel') - if (tokenData) { - const tokens: Tokens = { - accessToken: tokenData.accessToken, - expiresAt: tokenData.expiresAt?.getTime(), - } - session = await createSession(tokens) - } else { - session = existingSession - } - } else { - session = undefined - } - const response = new Response(JSON.stringify(await getData(session)), { headers: { 'Content-Type': 'application/json' }, }) - // Use the appropriate saveSession function based on auth provider - if (session && session.authProvider === 'github') { - await saveGitHubSession(response, session, authCookiePolicy) - } else { - await saveSession(response, session, authCookiePolicy) - } - + await saveSession(response, session, authCookiePolicy) return response } diff --git a/app/api/auth/signin/vercel/route.ts b/app/api/auth/signin/vercel/route.ts deleted file mode 100644 index 9a340d8..0000000 --- a/app/api/auth/signin/vercel/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { type NextRequest } from 'next/server' -import { cookies } from 'next/headers' -import { getAppBaseUrl } from '@/lib/auth/oauth' -import { isRelativeUrl } from '@/lib/utils/is-relative-url' -import { CodeChallengeMethod, OAuth2Client, generateCodeVerifier, generateState } from 'arctic' -import { getAuthCookiePolicyFromRequest, getAuthCookieSameSite, getAuthCookieSecure } from '@/lib/auth/cookie-policy' - -export async function POST(req: NextRequest): Promise { - const client = new OAuth2Client( - process.env.NEXT_PUBLIC_VERCEL_CLIENT_ID ?? '', - process.env.VERCEL_CLIENT_SECRET ?? '', - `${getAppBaseUrl(req)}/api/auth/callback/vercel`, - ) - - const state = generateState() - const verifier = generateCodeVerifier() - const url = client.createAuthorizationURLWithPKCE( - 'https://vercel.com/oauth/authorize', - state, - CodeChallengeMethod.S256, - verifier, - [], // Vercel uses default scopes - ) - - const store = await cookies() - const authCookiePolicy = getAuthCookiePolicyFromRequest(req) - const redirectTo = isRelativeUrl(req.nextUrl.searchParams.get('next') ?? '/') - ? (req.nextUrl.searchParams.get('next') ?? '/') - : '/' - - for (const [key, value] of [ - [`vercel_oauth_redirect_to`, redirectTo], - [`vercel_oauth_state`, state], - [`vercel_oauth_code_verifier`, verifier], - ]) { - store.set(key, value, { - path: '/', - secure: getAuthCookieSecure(authCookiePolicy), - httpOnly: true, - maxAge: 60 * 10, - sameSite: getAuthCookieSameSite(authCookiePolicy), - }) - } - - return Response.json({ url }) -} diff --git a/app/api/auth/signout/route.ts b/app/api/auth/signout/route.ts index 55e54eb..7fe8bc9 100644 --- a/app/api/auth/signout/route.ts +++ b/app/api/auth/signout/route.ts @@ -2,7 +2,7 @@ import type { NextRequest } from 'next/server' import { getGitHubClientId } from '@/lib/auth/oauth' import { getSessionFromReq } from '@/lib/session/server' import { isRelativeUrl } from '@/lib/utils/is-relative-url' -import { saveSession } from '@/lib/session/create' +import { saveSession } from '@/lib/session/create-github' import { getOAuthToken } from '@/lib/session/get-oauth-token' import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy' @@ -10,42 +10,21 @@ export async function GET(req: NextRequest) { const session = await getSessionFromReq(req) const authCookiePolicy = getAuthCookiePolicyFromRequest(req) if (session) { - // Check which provider the user authenticated with - if (session.authProvider === 'github') { - // Revoke GitHub token - fetch from database - try { - const tokenData = await getOAuthToken(session.user.id, 'github') - const clientId = getGitHubClientId() - if (tokenData && clientId && process.env.GITHUB_CLIENT_SECRET) { - await fetch(`https://api.github.com/applications/${clientId}/token`, { - method: 'DELETE', - headers: { - Authorization: `Basic ${Buffer.from(`${clientId}:${process.env.GITHUB_CLIENT_SECRET}`).toString('base64')}`, - Accept: 'application/vnd.github.v3+json', - }, - body: JSON.stringify({ access_token: tokenData.accessToken }), - }) - } - } catch { - console.error('Failed to revoke GitHub token') - } - } else { - // Revoke Vercel token - fetch from database - try { - const tokenData = await getOAuthToken(session.user.id, 'vercel') - if (tokenData) { - await fetch('https://vercel.com/api/login/oauth/token/revoke', { - method: 'POST', - body: new URLSearchParams({ token: tokenData.accessToken }), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${Buffer.from(`${process.env.NEXT_PUBLIC_VERCEL_CLIENT_ID}:${process.env.VERCEL_CLIENT_SECRET}`).toString('base64')}`, - }, - }) - } - } catch { - console.error('Failed to revoke Vercel token') + try { + const tokenData = await getOAuthToken(session.user.id, 'github') + const clientId = getGitHubClientId() + if (tokenData && clientId && process.env.GITHUB_CLIENT_SECRET) { + await fetch(`https://api.github.com/applications/${clientId}/token`, { + method: 'DELETE', + headers: { + Authorization: `Basic ${Buffer.from(`${clientId}:${process.env.GITHUB_CLIENT_SECRET}`).toString('base64')}`, + Accept: 'application/vnd.github.v3+json', + }, + body: JSON.stringify({ access_token: tokenData.accessToken }), + }) } + } catch { + console.error('Failed to revoke GitHub token') } } diff --git a/app/api/vercel/teams/route.ts b/app/api/vercel/teams/route.ts deleted file mode 100644 index b88ebb9..0000000 --- a/app/api/vercel/teams/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { NextResponse } from 'next/server' -import { getServerSession } from '@/lib/session/get-server-session' -import { getOAuthToken } from '@/lib/session/get-oauth-token' -import { fetchTeams } from '@/lib/vercel-client/teams' -import { fetchUser } from '@/lib/vercel-client/user' - -export async function GET() { - try { - const session = await getServerSession() - - if (!session?.user?.id || session.authProvider !== 'vercel') { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - // Get Vercel access token - const tokenData = await getOAuthToken(session.user.id, 'vercel') - if (!tokenData) { - return NextResponse.json({ error: 'No Vercel token found' }, { status: 401 }) - } - - // Fetch user info and teams - const [user, teams] = await Promise.all([fetchUser(tokenData.accessToken), fetchTeams(tokenData.accessToken)]) - - if (!user) { - return NextResponse.json({ error: 'Failed to fetch user info' }, { status: 500 }) - } - - // Build scopes list: personal account + teams - const scopes = [ - { - id: user.uid || user.id || '', - slug: user.username, - name: user.name || user.username, - type: 'personal' as const, - }, - ...(teams || []).map((team) => ({ - id: team.id, - slug: team.slug, - name: team.name, - type: 'team' as const, - })), - ] - - return NextResponse.json({ scopes }) - } catch (error) { - console.error('Error fetching Vercel teams:', error) - return NextResponse.json({ error: 'Failed to fetch Vercel teams' }, { status: 500 }) - } -} diff --git a/app/repos/[owner]/[repo]/layout.tsx b/app/repos/[owner]/[repo]/layout.tsx index 501a389..d009b8d 100644 --- a/app/repos/[owner]/[repo]/layout.tsx +++ b/app/repos/[owner]/[repo]/layout.tsx @@ -1,5 +1,4 @@ import { RepoLayout } from '@/components/repo-layout' -import { getServerSession } from '@/lib/session/get-server-session' import { getGitHubStars } from '@/lib/github-stars' import { Metadata } from 'next' @@ -13,17 +12,10 @@ interface LayoutPageProps { export default async function Layout({ params, children }: LayoutPageProps) { const { owner, repo } = await params - const session = await getServerSession() const stars = await getGitHubStars() return ( - + {children} ) diff --git a/app/tasks/[taskId]/page.tsx b/app/tasks/[taskId]/page.tsx index 7556189..81b3504 100644 --- a/app/tasks/[taskId]/page.tsx +++ b/app/tasks/[taskId]/page.tsx @@ -19,15 +19,7 @@ export default async function TaskPage({ params }: TaskPageProps) { const stars = await getGitHubStars() - return ( - - ) + return } export async function generateMetadata({ params }: TaskPageProps): Promise { diff --git a/app/tasks/page.tsx b/app/tasks/page.tsx index 91a69ad..4d6927d 100644 --- a/app/tasks/page.tsx +++ b/app/tasks/page.tsx @@ -12,5 +12,5 @@ export default async function TasksListPage() { redirect('/') } - return + return } diff --git a/components/auth/session-provider.tsx b/components/auth/session-provider.tsx index d3ac259..8e7c637 100644 --- a/components/auth/session-provider.tsx +++ b/components/auth/session-provider.tsx @@ -20,8 +20,8 @@ export function SessionProvider() { const data: SessionUserInfo = await response.json() setSession(data) setInitialized(true) - } catch (error) { - console.error('Failed to fetch session:', error) + } catch { + console.error('Failed to fetch session') setSession({ user: undefined }) setInitialized(true) } @@ -33,8 +33,8 @@ export function SessionProvider() { const data: GitHubConnection = await response.json() setGitHubConnection(data) setGitHubInitialized(true) - } catch (error) { - console.error('Failed to fetch GitHub connection:', error) + } catch { + console.error('Failed to fetch GitHub connection') setGitHubConnection({ connected: false }) setGitHubInitialized(true) } diff --git a/components/auth/sign-in.tsx b/components/auth/sign-in.tsx index ff1811c..fc49e37 100644 --- a/components/auth/sign-in.tsx +++ b/components/auth/sign-in.tsx @@ -2,26 +2,15 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { redirectToSignIn } from '@/lib/session/redirect-to-sign-in' import { GitHubIcon } from '@/components/icons/github-icon' import { useState } from 'react' -import { getEnabledAuthProviders } from '@/lib/auth/providers' import { GitHubPopupAuthError, startGitHubPopupAuth } from '@/lib/auth/github-popup' import { toast } from 'sonner' export function SignIn() { const [showDialog, setShowDialog] = useState(false) - const [loadingVercel, setLoadingVercel] = useState(false) const [loadingGitHub, setLoadingGitHub] = useState(false) - // Check which auth providers are enabled - const { github: hasGitHub, vercel: hasVercel } = getEnabledAuthProviders() - - const handleVercelSignIn = async () => { - setLoadingVercel(true) - await redirectToSignIn() - } - const handleGitHubSignIn = async () => { setLoadingGitHub(true) try { @@ -47,99 +36,48 @@ export function SignIn() { Sign in - - {hasGitHub && hasVercel - ? 'Choose how you want to sign in to continue.' - : hasVercel - ? 'Sign in with Vercel to continue.' - : 'Sign in with GitHub to continue.'} - + Sign in with GitHub to continue.
- {hasVercel && ( - - )} - - {hasGitHub && ( - - )} +
diff --git a/components/auth/sign-out.tsx b/components/auth/sign-out.tsx index 364290a..c81a3e7 100644 --- a/components/auth/sign-out.tsx +++ b/components/auth/sign-out.tsx @@ -12,17 +12,14 @@ import { import { redirectToSignOut } from '@/lib/session/redirect-to-sign-out' import { toast } from 'sonner' import { useRouter } from 'next/navigation' -import { useSetAtom, useAtomValue } from 'jotai' +import { useSetAtom } from 'jotai' import { sessionAtom } from '@/lib/atoms/session' -import { githubConnectionAtom } from '@/lib/atoms/github-connection' import { GitHubIcon } from '@/components/icons/github-icon' import { ApiKeysDialog } from '@/components/api-keys-dialog' import { SandboxesDialog } from '@/components/sandboxes-dialog' import { ThemeToggle } from '@/components/theme-toggle' import { Key, Server } from 'lucide-react' import { useState, useEffect, useCallback } from 'react' -import { getEnabledAuthProviders } from '@/lib/auth/providers' -import { GitHubPopupAuthError, startGitHubPopupAuth } from '@/lib/auth/github-popup' interface RateLimitInfo { used: number @@ -30,18 +27,12 @@ interface RateLimitInfo { remaining: number } -export function SignOut({ user, authProvider }: Pick) { +export function SignOut({ user }: Pick) { const router = useRouter() const setSession = useSetAtom(sessionAtom) - const githubConnection = useAtomValue(githubConnectionAtom) - const setGitHubConnection = useSetAtom(githubConnectionAtom) const [showApiKeysDialog, setShowApiKeysDialog] = useState(false) const [showSandboxesDialog, setShowSandboxesDialog] = useState(false) const [rateLimit, setRateLimit] = useState(null) - const [loadingGitHub, setLoadingGitHub] = useState(false) - - // Check which auth providers are enabled - const { github: hasGitHub } = getEnabledAuthProviders() const handleSignOut = async () => { await redirectToSignOut() @@ -50,38 +41,6 @@ export function SignOut({ user, authProvider }: Pick { - try { - const response = await fetch('/api/auth/github/disconnect', { method: 'POST' }) - if (response.ok) { - // Immediately update the atom to reflect disconnected state - setGitHubConnection({ connected: false }) - toast.success('GitHub disconnected') - router.refresh() - } else { - toast.error('Failed to disconnect GitHub') - } - } catch { - console.error('Failed to disconnect GitHub') - toast.error('Failed to disconnect GitHub') - } - } - - const handleGitHubConnect = async () => { - setLoadingGitHub(true) - try { - await startGitHubPopupAuth('/api/auth/github/signin') - window.location.reload() - } catch (error) { - if (error instanceof GitHubPopupAuthError && error.code === 'popup_blocked') { - toast.error('Please allow popups and try again.') - } else { - toast.error('GitHub authentication failed. Please try again.') - } - setLoadingGitHub(false) - } - } - // Fetch rate limit info on mount useEffect(() => { let mounted = true @@ -162,39 +121,9 @@ export function SignOut({ user, authProvider }: Pick - {/* Only show GitHub Connect/Disconnect for Vercel users when GitHub is enabled */} - {authProvider === 'vercel' && hasGitHub && ( - <> - {githubConnection.connected ? ( - - - Disconnect - - ) : ( - - - {loadingGitHub ? 'Connecting...' : 'Connect'} - - )} - - )} - - - - {authProvider === 'github' ? ( - <> - - Log Out - - ) : ( - <> - - - - Log Out - - )} + + Log Out diff --git a/components/auth/user.tsx b/components/auth/user.tsx index 527a341..60111db 100644 --- a/components/auth/user.tsx +++ b/components/auth/user.tsx @@ -7,7 +7,7 @@ import { useAtomValue } from 'jotai' import { sessionAtom, sessionInitializedAtom } from '@/lib/atoms/session' import { useMemo } from 'react' -export function User(props: { user?: Session['user'] | null; authProvider?: Session['authProvider'] | null }) { +export function User(props: { user?: Session['user'] | null }) { const session = useAtomValue(sessionAtom) const initialized = useAtomValue(sessionInitializedAtom) @@ -16,13 +16,9 @@ export function User(props: { user?: Session['user'] | null; authProvider?: Sess () => (initialized ? (session.user ?? null) : (props.user ?? null)), [initialized, session.user, props.user], ) - const authProvider = useMemo( - () => (initialized ? (session.authProvider ?? 'vercel') : (props.authProvider ?? 'vercel')), - [initialized, session.authProvider, props.authProvider], - ) if (user) { - return + return } else { return } diff --git a/components/repo-layout.tsx b/components/repo-layout.tsx index b8c1e18..d9098fd 100644 --- a/components/repo-layout.tsx +++ b/components/repo-layout.tsx @@ -4,7 +4,6 @@ import { usePathname, useRouter } from 'next/navigation' import Link from 'next/link' import { SharedHeader } from '@/components/shared-header' import { Button } from '@/components/ui/button' -import type { Session } from '@/lib/session/types' import { cn } from '@/lib/utils' import { setSelectedOwner, setSelectedRepo } from '@/lib/utils/cookies' import { Plus } from 'lucide-react' @@ -12,13 +11,11 @@ import { Plus } from 'lucide-react' interface RepoLayoutProps { owner: string repo: string - user: Session['user'] | null - authProvider: Session['authProvider'] | null initialStars?: number children: React.ReactNode } -export function RepoLayout({ owner, repo, user, authProvider, initialStars = 1200, children }: RepoLayoutProps) { +export function RepoLayout({ owner, repo, initialStars = 1200, children }: RepoLayoutProps) { const pathname = usePathname() const router = useRouter() diff --git a/components/repo-page-client.tsx b/components/repo-page-client.tsx index a47fd2c..97bdda8 100644 --- a/components/repo-page-client.tsx +++ b/components/repo-page-client.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { SharedHeader } from '@/components/shared-header' -import type { Session } from '@/lib/session/types' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { RepoCommits } from '@/components/repo-commits' import { RepoPullRequests } from '@/components/repo-pull-requests' @@ -11,12 +10,10 @@ import { GitBranch, GitPullRequest } from 'lucide-react' interface RepoPageClientProps { owner: string repo: string - user: Session['user'] | null - authProvider: Session['authProvider'] | null initialStars?: number } -export function RepoPageClient({ owner, repo, user, authProvider, initialStars = 1200 }: RepoPageClientProps) { +export function RepoPageClient({ owner, repo, initialStars = 1200 }: RepoPageClientProps) { const [activeTab, setActiveTab] = useState('commits') const headerLeftActions = ( diff --git a/components/repo-selector.tsx b/components/repo-selector.tsx index 4cc66a6..dba813f 100644 --- a/components/repo-selector.tsx +++ b/components/repo-selector.tsx @@ -117,23 +117,13 @@ export function RepoSelector({ // Fetch both user and organizations const [userResponse, orgsResponse] = await Promise.all([fetch('/api/github/user'), fetch('/api/github/orgs')]) - // Check for authentication errors - disconnect GitHub if auth fails + // Check for authentication errors and mark the local GitHub session unavailable. if (!userResponse.ok) { if (userResponse.status === 401 || userResponse.status === 403) { // Clear cache using atoms setOwners(null) - // Call backend to disconnect GitHub - try { - await fetch('/api/auth/github/disconnect', { - method: 'POST', - credentials: 'include', - }) - } catch (error) { - console.error('Error disconnecting GitHub:', error) - } - - // Update connection state to trigger "Connect GitHub" button + // Update connection state to show the GitHub session unavailable state. setGitHubConnection({ connected: false }) setLoadingOwners(false) setIsRefreshing(false) @@ -171,18 +161,8 @@ export function RepoSelector({ setOwners(sortedOwners) // Cache is automatic with atomWithStorage - } catch (error) { - console.error('Error loading owners:', error) - - // Call backend to disconnect GitHub - try { - await fetch('/api/auth/github/disconnect', { - method: 'POST', - credentials: 'include', - }) - } catch (disconnectError) { - console.error('Error disconnecting GitHub:', disconnectError) - } + } catch { + console.error('Error loading owners') // On any error, clear the connection setGitHubConnection({ connected: false }) @@ -260,8 +240,8 @@ export function RepoSelector({ setTemporaryOwner(null) setTemporaryRepo(null) } - } catch (error) { - console.error('Error verifying external repo:', error) + } catch { + console.error('Error verifying external repo') setTemporaryOwner(null) setTemporaryRepo(null) } @@ -308,17 +288,7 @@ export function RepoSelector({ // Clear cache using atoms setOwners(null) - // Call backend to disconnect GitHub - try { - await fetch('/api/auth/github/disconnect', { - method: 'POST', - credentials: 'include', - }) - } catch (error) { - console.error('Error disconnecting GitHub:', error) - } - - // Update connection state to trigger "Connect GitHub" button + // Update connection state to show the GitHub session unavailable state. setGitHubConnection({ connected: false }) setLoadingRepos(false) setIsRefreshing(false) @@ -330,18 +300,8 @@ export function RepoSelector({ const reposList = await response.json() setRepos(reposList) // Cache is automatic with atomWithStorage - } catch (error) { - console.error('Error loading repos:', error) - - // Call backend to disconnect GitHub - try { - await fetch('/api/auth/github/disconnect', { - method: 'POST', - credentials: 'include', - }) - } catch (disconnectError) { - console.error('Error disconnecting GitHub:', disconnectError) - } + } catch { + console.error('Error loading repos') // On any error, clear the connection setGitHubConnection({ connected: false }) diff --git a/components/sealos-home-page-content.tsx b/components/sealos-home-page-content.tsx index 7924279..6184678 100644 --- a/components/sealos-home-page-content.tsx +++ b/components/sealos-home-page-content.tsx @@ -12,8 +12,6 @@ import { TaskForm } from '@/components/task-form' import { GitHubIcon } from '@/components/icons/github-icon' import { useTasks } from '@/components/app-layout' import { setSelectedOwner, setSelectedRepo } from '@/lib/utils/cookies' -import { redirectToSignIn } from '@/lib/session/redirect-to-sign-in' -import { getEnabledAuthProviders } from '@/lib/auth/providers' import { taskPromptAtom } from '@/lib/atoms/task' import { sessionAtom } from '@/lib/atoms/session' import { githubConnectionAtom, githubConnectionInitializedAtom } from '@/lib/atoms/github-connection' @@ -45,7 +43,6 @@ export function SealosHomePageContent({ const [selectedOwner, setSelectedOwnerState] = useState(initialSelectedOwner) const [selectedRepo, setSelectedRepoState] = useState(initialSelectedRepo) const [showSignInDialog, setShowSignInDialog] = useState(false) - const [loadingVercel, setLoadingVercel] = useState(false) const [loadingGitHub, setLoadingGitHub] = useState(false) const router = useRouter() const { refreshTasks, addTaskOptimistically } = useTasks() @@ -54,7 +51,6 @@ export function SealosHomePageContent({ const githubConnection = useAtomValue(githubConnectionAtom) const githubConnectionInitialized = useAtomValue(githubConnectionInitializedAtom) const isGitHubAuthUser = session.authProvider === 'github' - const { github: hasGitHub, vercel: hasVercel } = getEnabledAuthProviders() const isAuthenticated = Boolean(user) const visibleSelectedOwner = isAuthenticated ? selectedOwner : '' const visibleSelectedRepo = isAuthenticated ? selectedRepo : '' @@ -140,11 +136,6 @@ export function SealosHomePageContent({ } } - const handleVercelSignIn = async () => { - setLoadingVercel(true) - await redirectToSignIn() - } - const handleGitHubSignIn = async () => { setLoadingGitHub(true) try { @@ -160,21 +151,6 @@ export function SealosHomePageContent({ } } - const handleConnectGitHub = async () => { - setLoadingGitHub(true) - try { - await startGitHubPopupAuth('/api/auth/github/signin') - window.location.reload() - } catch (error) { - if (error instanceof GitHubPopupAuthError && error.code === 'popup_blocked') { - toast.error('Please allow popups and try again.') - } else { - toast.error('GitHub authentication failed. Please try again.') - } - setLoadingGitHub(false) - } - } - const openSignIn = () => { setShowSignInDialog(true) } @@ -196,7 +172,7 @@ export function SealosHomePageContent({ ? '' : canSelectRepository ? 'Select a repository above to continue.' - : 'Connect GitHub to choose a repository.' + : 'GitHub session unavailable. Sign out and sign in again.' const commandHeader = (
@@ -218,16 +194,7 @@ export function SealosHomePageContent({ />
) : ( - +
GitHub session unavailable. Sign out and sign in again.
)} @@ -280,80 +247,37 @@ export function SealosHomePageContent({ Sign in to continue - - {hasGitHub && hasVercel - ? 'You need to sign in to create tasks. Choose how you want to sign in.' - : hasVercel - ? 'You need to sign in with Vercel to create tasks.' - : 'You need to sign in with GitHub to create tasks.'} - + You need to sign in with GitHub to create tasks.
- {hasVercel && ( - - )} - - {hasGitHub && ( - - )} +
diff --git a/components/sealos-task-page-client.tsx b/components/sealos-task-page-client.tsx index 88bbdb3..a4d89cc 100644 --- a/components/sealos-task-page-client.tsx +++ b/components/sealos-task-page-client.tsx @@ -5,12 +5,9 @@ import { useTask } from '@/lib/hooks/use-task' import { TaskChat } from '@/components/task-chat' import { SharedHeader } from '@/components/shared-header' import { TaskActions } from '@/components/task-actions' -import type { Session } from '@/lib/session/types' interface SealosTaskPageClientProps { taskId: string - user: Session['user'] | null - authProvider: Session['authProvider'] | null initialStars?: number maxSandboxDuration?: number } diff --git a/components/task-form.tsx b/components/task-form.tsx index 0eda820..628033e 100644 --- a/components/task-form.tsx +++ b/components/task-form.tsx @@ -157,7 +157,7 @@ export function TaskForm({ ? 'Sign in first, then choose a GitHub repository and tell Sealos how it should be analyzed, built, and deployed.' : hasSelectedRepo ? 'Tell Sealos what you want to do with this repository. A simple deployment request is enough.' - : 'Connect GitHub if needed, choose a repository, then describe how Sealos should analyze, build, and deploy it.' + : 'Sign in with GitHub, choose a repository, then describe how Sealos should analyze, build, and deploy it.' const repoBannerText = hasSelectedRepo ? `${selectedOwner}/${selectedRepo}` diff --git a/components/task-page-client.tsx b/components/task-page-client.tsx index b6a8429..ad55a8c 100644 --- a/components/task-page-client.tsx +++ b/components/task-page-client.tsx @@ -6,12 +6,9 @@ import { TaskDetails } from '@/components/task-details' import { SharedHeader } from '@/components/shared-header' import { TaskActions } from '@/components/task-actions' import { LogsPane } from '@/components/logs-pane' -import type { Session } from '@/lib/session/types' interface TaskPageClientProps { taskId: string - user: Session['user'] | null - authProvider: Session['authProvider'] | null initialStars?: number maxSandboxDuration?: number } @@ -33,13 +30,7 @@ function parseRepoFromUrl(repoUrl: string | null): { owner: string; repo: string } } -export function TaskPageClient({ - taskId, - user, - authProvider, - initialStars = 1200, - maxSandboxDuration = 300, -}: TaskPageClientProps) { +export function TaskPageClient({ taskId, initialStars = 1200, maxSandboxDuration = 300 }: TaskPageClientProps) { const { task, isLoading, error } = useTask(taskId) const [logsPaneHeight, setLogsPaneHeight] = useState(40) // Default to collapsed height diff --git a/components/task-sidebar.tsx b/components/task-sidebar.tsx index 02429fa..8349aeb 100644 --- a/components/task-sidebar.tsx +++ b/components/task-sidebar.tsx @@ -540,7 +540,7 @@ export function TaskSidebar({ tasks, width = 288 }: TaskSidebarProps) { {!githubConnection.connected ? ( - Connect GitHub to view your repositories + GitHub session unavailable. Sign out and sign in again. ) : (reposLoading && repos.length === 0 && !isSearching) || diff --git a/components/tasks-list-client.tsx b/components/tasks-list-client.tsx index 2e18404..4fc61b8 100644 --- a/components/tasks-list-client.tsx +++ b/components/tasks-list-client.tsx @@ -22,13 +22,10 @@ import { AlertCircle, Trash2, Square, StopCircle, CheckSquare, X, Clock } from ' import { toast } from 'sonner' import { useRouter } from 'next/navigation' import { cn } from '@/lib/utils' -import type { Session } from '@/lib/session/types' import { PRStatusIcon } from '@/components/pr-status-icon' import { PRCheckStatus } from '@/components/pr-check-status' interface TasksListClientProps { - user: Session['user'] | null - authProvider: Session['authProvider'] | null initialStars?: number } @@ -49,7 +46,7 @@ function getTimeAgo(date: Date): string { return new Date(date).toLocaleDateString() } -export function TasksListClient({ user, authProvider, initialStars = 1200 }: TasksListClientProps) { +export function TasksListClient({ initialStars = 1200 }: TasksListClientProps) { const { toggleSidebar, refreshTasks } = useTasks() const router = useRouter() const [tasks, setTasks] = useState([]) diff --git a/lib/auth/account-merge.ts b/lib/auth/account-merge.ts deleted file mode 100644 index 7f94400..0000000 --- a/lib/auth/account-merge.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { keys } from '@/lib/db/schema' - -type KeyProvider = (typeof keys.$inferSelect)['provider'] - -interface MergeKeyRow { - id: string - provider: KeyProvider -} - -export function planUserKeyMerge({ - sourceKeys, - targetKeys, -}: { - sourceKeys: MergeKeyRow[] - targetKeys: MergeKeyRow[] -}): { - deleteKeyIds: string[] - moveKeyIds: string[] -} { - const targetProviders = new Set(targetKeys.map((key) => key.provider)) - const moveKeyIds: string[] = [] - const deleteKeyIds: string[] = [] - - for (const key of sourceKeys) { - if (targetProviders.has(key.provider)) { - deleteKeyIds.push(key.id) - } else { - moveKeyIds.push(key.id) - } - } - - return { - deleteKeyIds, - moveKeyIds, - } -} diff --git a/lib/auth/iframe-oauth.test.ts b/lib/auth/iframe-oauth.test.ts index 4db4067..ba13252 100644 --- a/lib/auth/iframe-oauth.test.ts +++ b/lib/auth/iframe-oauth.test.ts @@ -54,21 +54,6 @@ test('public GitHub sign-in route preserves sign-in intent inside iframes', () = assert.doesNotMatch(source, /authMode = isSignInFlow \? 'signin' : 'connect'/) }) -test('account merge does not move duplicate key providers onto the target user', async () => { - const { planUserKeyMerge } = await import('./account-merge') - - const plan = planUserKeyMerge({ - sourceKeys: [ - { id: 'source-aiproxy-key', provider: 'aiproxy' }, - { id: 'source-openai-key', provider: 'openai' }, - ], - targetKeys: [{ id: 'target-aiproxy-key', provider: 'aiproxy' }], - }) - - assert.deepEqual(plan.moveKeyIds, ['source-openai-key']) - assert.deepEqual(plan.deleteKeyIds, ['source-aiproxy-key']) -}) - test('GitHub callback notifies embedded opener frames before closing popup', () => { const source = readRepoFile('app/api/auth/github/callback/route.ts') diff --git a/lib/auth/oauth-removal-regression.test.ts b/lib/auth/oauth-removal-regression.test.ts new file mode 100644 index 0000000..358ba97 --- /dev/null +++ b/lib/auth/oauth-removal-regression.test.ts @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { test } from 'node:test' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Session } from '@/lib/session/types' + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') + +function readRepoFile(path: string): string { + return readFileSync(join(repoRoot, path), 'utf8') +} + +test('legacy Vercel session cookies are rejected', async () => { + const previousSecret = process.env.JWE_SECRET + process.env.JWE_SECRET = Buffer.from('0123456789abcdef0123456789abcdef').toString('base64url') + + try { + const { encryptJWE } = await import('@/lib/jwe/encrypt') + const { getSessionFromCookie } = await import('@/lib/session/server') + const legacySession = { + created: Date.now(), + authProvider: 'vercel', + user: { + id: 'legacy-user', + username: 'legacy', + email: undefined, + avatar: 'https://example.com/avatar.png', + }, + } as unknown as Session + + const cookieValue = await encryptJWE(legacySession, '1h') + + assert.equal(await getSessionFromCookie(cookieValue), undefined) + } finally { + if (previousSecret === undefined) { + delete process.env.JWE_SECRET + } else { + process.env.JWE_SECRET = previousSecret + } + } +}) + +test('legacy GitHub account refresh updates the selected account row by id', () => { + const source = readRepoFile('lib/db/users.ts') + + assert.match(source, /select\(\{\s*id: accounts\.id,\s*userId: accounts\.userId,?\s*\}\)/) + assert.match(source, /\.where\(eq\(accounts\.id, existingAccount\[0\]\.id\)\)/) + assert.doesNotMatch( + source, + /update\(accounts\)[\s\S]*?\.where\(and\(eq\(accounts\.provider, 'github'\), eq\(accounts\.externalUserId, externalId\)\)\)/, + ) +}) diff --git a/lib/auth/providers.ts b/lib/auth/providers.ts deleted file mode 100644 index 7e36541..0000000 --- a/lib/auth/providers.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Get the list of enabled authentication providers from environment variables - * Defaults to GitHub only if not specified - */ -export function getEnabledAuthProviders(): { - github: boolean - vercel: boolean -} { - const providers = process.env.NEXT_PUBLIC_AUTH_PROVIDERS || 'github' - const enabledProviders = providers.split(',').map((p) => p.trim().toLowerCase()) - - return { - github: enabledProviders.includes('github'), - vercel: enabledProviders.includes('vercel'), - } -} diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 61424c2..1fe5920 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -298,10 +298,8 @@ export const selectConnectorSchema = z.object({ export type Connector = z.infer export type InsertConnector = z.infer -// Accounts table - Additional accounts linked to users -// Currently only GitHub can be connected as an additional account -// (e.g., Vercel users can connect their GitHub account) -// Multiple users can connect to the same external account (each as a separate record) +// Accounts table - additional OAuth accounts linked to users. +// Kept for legacy users who connected GitHub before GitHub-only sign-in. export const accounts = pgTable( 'accounts', { diff --git a/lib/db/users.ts b/lib/db/users.ts index daccf3f..fe992dd 100644 --- a/lib/db/users.ts +++ b/lib/db/users.ts @@ -9,8 +9,8 @@ import { generateId } from '@/lib/utils/id' * Find or create a user in the database * Returns the internal user ID (our generated ID, not the external auth provider ID) * - * IMPORTANT: This checks if the externalId is already connected to an existing user via accounts - * to prevent duplicate accounts when someone connects GitHub then later signs in with GitHub + * IMPORTANT: This checks if a GitHub externalId is already connected to an existing legacy user via accounts + * to preserve their internal user ID when they later sign in directly with GitHub. */ export async function upsertUser( userData: Omit, @@ -44,29 +44,38 @@ export async function upsertUser( return existingUser[0].id } - // Second check: Is this a GitHub account already connected to an existing user via accounts table? - // This prevents duplicate accounts when someone: - // 1. Signs in with Vercel - // 2. Connects GitHub - // 3. Later signs in directly with GitHub + // This preserves legacy users who connected GitHub as a secondary account + // before GitHub became the only supported sign-in method. if (provider === 'github') { const existingAccount = await db - .select({ userId: accounts.userId }) + .select({ id: accounts.id, userId: accounts.userId }) .from(accounts) .where(and(eq(accounts.provider, 'github'), eq(accounts.externalUserId, externalId))) .limit(1) if (existingAccount.length > 0) { console.log('GitHub account is already connected to an existing user') + const now = new Date() - // Update the existing user's last login - await db - .update(users) - .set({ - updatedAt: new Date(), - lastLoginAt: new Date(), - }) - .where(eq(users.id, existingAccount[0].userId)) + await db.transaction(async (tx) => { + await tx + .update(accounts) + .set({ + accessToken, + scope, + username: userData.username, + updatedAt: now, + }) + .where(eq(accounts.id, existingAccount[0].id)) + + await tx + .update(users) + .set({ + updatedAt: now, + lastLoginAt: now, + }) + .where(eq(users.id, existingAccount[0].userId)) + }) return existingAccount[0].userId } @@ -98,7 +107,7 @@ export async function getUserById(userId: string) { /** * Get user by auth provider and external ID */ -export async function getUserByExternalId(provider: 'github' | 'vercel', externalId: string) { +export async function getUserByExternalId(provider: 'github', externalId: string) { const result = await db .select() .from(users) diff --git a/lib/session/create.ts b/lib/session/create.ts deleted file mode 100644 index 3825963..0000000 --- a/lib/session/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import 'server-only' - -import type { Session, Tokens } from './types' -import { SESSION_COOKIE_NAME } from './constants' -import { encryptJWE } from '@/lib/jwe/encrypt' -import { fetchUser } from '@/lib/vercel-client/user' -import { upsertUser } from '@/lib/db/users' -import { encrypt } from '@/lib/crypto' -import { getAuthCookieHeaderAttributes, type AuthCookiePolicyInput } from '@/lib/auth/cookie-policy' -import ms from 'ms' - -export async function createSession(tokens: Tokens): Promise { - const user = await fetchUser(tokens.accessToken) - - if (!user) { - console.log('Failed to fetch user') - return undefined - } - - // Create or update user in database - const externalId = user.uid || user.id || '' - const userId = await upsertUser({ - provider: 'vercel', - externalId, - accessToken: encrypt(tokens.accessToken), // Encrypt before storing - refreshToken: tokens.refreshToken ? encrypt(tokens.refreshToken) : undefined, // Encrypt if present - scope: undefined, // Vercel doesn't provide scope - username: user.username, - email: user.email, - name: user.name, - avatarUrl: `https://vercel.com/api/www/avatar/?u=${user.username}`, - }) - - const session = { - created: Date.now(), - authProvider: 'vercel' as const, - user: { - id: userId, // Internal user ID - username: user.username, - email: user.email, - name: user.name, - avatar: `https://vercel.com/api/www/avatar/?u=${user.username}`, - }, - } - - console.log('Created session') - return session -} - -const COOKIE_TTL = ms('1y') - -export async function saveSession( - res: Response, - session: Session | undefined, - authCookiePolicy?: AuthCookiePolicyInput, -): Promise { - if (!session) { - res.headers.append( - 'Set-Cookie', - `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; ${getAuthCookieHeaderAttributes(authCookiePolicy)}`, - ) - return - } - - const value = await encryptJWE(session, '1y') - const expires = new Date(Date.now() + COOKIE_TTL).toUTCString() - res.headers.append( - 'Set-Cookie', - `${SESSION_COOKIE_NAME}=${value}; Path=/; Max-Age=${COOKIE_TTL / 1000}; Expires=${expires}; HttpOnly; ${getAuthCookieHeaderAttributes(authCookiePolicy)}`, - ) - return value -} diff --git a/lib/session/get-oauth-token.ts b/lib/session/get-oauth-token.ts index 3042a31..60e41e8 100644 --- a/lib/session/get-oauth-token.ts +++ b/lib/session/get-oauth-token.ts @@ -5,80 +5,60 @@ import { users, accounts } from '@/lib/db/schema' import { eq, and } from 'drizzle-orm' import { decrypt } from '@/lib/crypto' -type OAuthProvider = 'github' | 'vercel' +type OAuthProvider = 'github' /** * Get the OAuth access token for a user from the database * Returns the decrypted token or null if not found * - * For GitHub: Checks accounts table first (connected account), then users table (primary account) - * For Vercel: Gets from users table (primary account only) + * Checks accounts table first for legacy connected GitHub accounts, + * then users table for GitHub primary accounts. */ export async function getOAuthToken( userId: string, provider: OAuthProvider, ): Promise<{ accessToken: string; refreshToken: string | null; expiresAt: Date | null } | null> { try { - if (provider === 'github') { - // Check if user has GitHub as a connected account - const account = await db - .select({ - accessToken: accounts.accessToken, - refreshToken: accounts.refreshToken, - expiresAt: accounts.expiresAt, - }) - .from(accounts) - .where(and(eq(accounts.userId, userId), eq(accounts.provider, 'github'))) - .limit(1) + // Check if user has GitHub as a connected account + const account = await db + .select({ + accessToken: accounts.accessToken, + refreshToken: accounts.refreshToken, + expiresAt: accounts.expiresAt, + }) + .from(accounts) + .where(and(eq(accounts.userId, userId), eq(accounts.provider, provider))) + .limit(1) - if (account[0]?.accessToken) { - return { - accessToken: decrypt(account[0].accessToken), - refreshToken: account[0].refreshToken ? decrypt(account[0].refreshToken) : null, - expiresAt: account[0].expiresAt, - } + if (account[0]?.accessToken) { + return { + accessToken: decrypt(account[0].accessToken), + refreshToken: account[0].refreshToken ? decrypt(account[0].refreshToken) : null, + expiresAt: account[0].expiresAt, } + } - // Fall back to checking if user signed in with GitHub (primary account) - const user = await db - .select({ - accessToken: users.accessToken, - refreshToken: users.refreshToken, - }) - .from(users) - .where(and(eq(users.id, userId), eq(users.provider, 'github'))) - .limit(1) - - if (user[0]?.accessToken) { - return { - accessToken: decrypt(user[0].accessToken), - refreshToken: user[0].refreshToken ? decrypt(user[0].refreshToken) : null, - expiresAt: null, // Users table doesn't have expiresAt - } - } - } else if (provider === 'vercel') { - // Vercel is only available as a primary account - const user = await db - .select({ - accessToken: users.accessToken, - refreshToken: users.refreshToken, - }) - .from(users) - .where(and(eq(users.id, userId), eq(users.provider, 'vercel'))) - .limit(1) + // Fall back to checking if user signed in with GitHub (primary account) + const user = await db + .select({ + accessToken: users.accessToken, + refreshToken: users.refreshToken, + }) + .from(users) + .where(and(eq(users.id, userId), eq(users.provider, provider))) + .limit(1) - if (user[0]?.accessToken) { - return { - accessToken: decrypt(user[0].accessToken), - refreshToken: user[0].refreshToken ? decrypt(user[0].refreshToken) : null, - expiresAt: null, // Users table doesn't have expiresAt - } + if (user[0]?.accessToken) { + return { + accessToken: decrypt(user[0].accessToken), + refreshToken: user[0].refreshToken ? decrypt(user[0].refreshToken) : null, + expiresAt: null, // Users table doesn't have expiresAt } } return null - } catch (error) { - console.error('Error fetching OAuth token:', error) + } catch { + console.error('Error fetching OAuth token') return null } } diff --git a/lib/session/redirect-to-sign-in.ts b/lib/session/redirect-to-sign-in.ts deleted file mode 100644 index d15b123..0000000 --- a/lib/session/redirect-to-sign-in.ts +++ /dev/null @@ -1,14 +0,0 @@ -export async function redirectToSignIn(): Promise { - const response = await fetch( - `/api/auth/signin/vercel?${new URLSearchParams({ - next: window.location.pathname, - }).toString()}`, - { method: 'POST' }, - ) - - const { url } = await response.json() - window.location = url - if (window.location.hash) { - window.location.reload() - } -} diff --git a/lib/session/server.ts b/lib/session/server.ts index 96a6a57..5c16521 100644 --- a/lib/session/server.ts +++ b/lib/session/server.ts @@ -4,15 +4,19 @@ import { SESSION_COOKIE_NAME } from './constants' import { decryptJWE } from '@/lib/jwe/decrypt' export async function getSessionFromCookie(cookieValue?: string): Promise { - if (cookieValue) { - const decrypted = await decryptJWE(cookieValue) - if (decrypted) { - return { - created: decrypted.created, - authProvider: decrypted.authProvider, - user: decrypted.user, - } - } + if (!cookieValue) { + return undefined + } + + const decrypted = await decryptJWE(cookieValue) + if (!decrypted || decrypted.authProvider !== 'github') { + return undefined + } + + return { + created: decrypted.created, + authProvider: 'github', + user: decrypted.user, } } diff --git a/lib/session/types.ts b/lib/session/types.ts index 87462e3..d6d9f19 100644 --- a/lib/session/types.ts +++ b/lib/session/types.ts @@ -1,17 +1,11 @@ export interface SessionUserInfo { user: User | undefined - authProvider?: 'github' | 'vercel' // Which provider the user signed in with -} - -export interface Tokens { - accessToken: string - expiresAt?: number - refreshToken?: string + authProvider?: 'github' } export interface Session { created: number - authProvider: 'github' | 'vercel' // Which provider the user signed in with + authProvider: 'github' user: User } diff --git a/lib/vercel-client/projects.ts b/lib/vercel-client/projects.ts deleted file mode 100644 index d82bd87..0000000 --- a/lib/vercel-client/projects.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Vercel } from '@vercel/sdk' - -interface CreateProjectParams { - name: string - gitRepository?: { - type: 'github' - repo: string // Format: "owner/repo" - } - framework?: string | null -} - -interface CreateProjectResponse { - id: string - name: string - accountId: string - framework: string | null - link?: { - type: string - repo: string - repoId: number - } -} - -/** - * Create a Vercel project using the official SDK - * @param accessToken - Vercel OAuth access token - * @param teamId - Team ID (for teams) or User ID (for personal accounts) - * @param params - Project creation parameters - * @returns The created project data - */ -export async function createProject( - accessToken: string, - teamId: string, - params: CreateProjectParams, -): Promise { - try { - const vercel = new Vercel({ - bearerToken: accessToken, - }) - - // Use the SDK as shown in the Vercel docs - const requestBody: Record = { - name: params.name, - gitRepository: params.gitRepository - ? { - type: params.gitRepository.type, - repo: params.gitRepository.repo, - } - : undefined, - } - - // Only add framework if it's provided - if (params.framework) { - requestBody.framework = params.framework - } - - const response = await vercel.projects.createProject({ - teamId, // Pass teamId at the top level - // eslint-disable-next-line @typescript-eslint/no-explicit-any - requestBody: requestBody as any, - }) - - console.log('Successfully created Vercel project') - return response as unknown as CreateProjectResponse - } catch (error) { - console.error('Error creating Vercel project:', error) - - // Check for permission errors - if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 403) { - console.error('Permission denied - user may need proper team permissions in Vercel') - } - - return undefined - } -} diff --git a/lib/vercel-client/teams.ts b/lib/vercel-client/teams.ts deleted file mode 100644 index 743257e..0000000 --- a/lib/vercel-client/teams.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { VercelTeam } from './types' - -export async function fetchTeams(accessToken: string) { - const response = await fetch('https://api.vercel.com/v2/teams', { - headers: { Authorization: `Bearer ${accessToken}` }, - cache: 'no-store', - }) - - if (response.status !== 200) { - const errorText = await response.text() - - // 403 is expected if user doesn't have team access - if (response.status === 403) { - console.log('User does not have team access (this is normal for personal accounts)') - return [] - } - - console.error('Failed to fetch teams', response.status, errorText) - return undefined - } - - const { teams } = (await response.json()) as { teams: VercelTeam[] } - console.log('Successfully fetched teams') - return teams || [] -} diff --git a/lib/vercel-client/types.ts b/lib/vercel-client/types.ts deleted file mode 100644 index 5a02e87..0000000 --- a/lib/vercel-client/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface VercelUser { - avatar: string - email: string - name: string - uid?: string - id?: string - username: string -} - -export interface VercelTeam { - avatar?: string - created?: string - id: string - name: string - saml?: { enforced: boolean } - slug: string -} diff --git a/lib/vercel-client/user.ts b/lib/vercel-client/user.ts deleted file mode 100644 index cfb139d..0000000 --- a/lib/vercel-client/user.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { VercelUser } from './types' - -export async function fetchUser(accessToken: string): Promise { - // Try the user endpoint - let response = await fetch('https://api.vercel.com/v2/user', { - headers: { Authorization: `Bearer ${accessToken}` }, - cache: 'no-store', - }) - - if (response.status !== 200) { - console.error('Failed to fetch user from v2 endpoint', response.status, await response.text()) - - // Fallback to www/user endpoint - response = await fetch('https://vercel.com/api/www/user', { - headers: { Authorization: `Bearer ${accessToken}` }, - cache: 'no-store', - }) - - if (response.status !== 200) { - console.error('Failed to fetch user from www endpoint', response.status, await response.text()) - return undefined - } - } - - // Try to parse response - format may vary by endpoint - const data = (await response.json()) as { user?: VercelUser } | VercelUser - const user: VercelUser | undefined = 'user' in data && data.user ? data.user : 'username' in data ? data : undefined - - if (!user) { - console.error('No user data in response') - return undefined - } - - return user -} diff --git a/package.json b/package.json index 86fa600..2b1c540 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "@types/js-cookie": "^3.0.6", "@types/ws": "^8.18.1", "@vercel/analytics": "^1.6.1", - "@vercel/sdk": "^1.18.7", "@vercel/speed-insights": "^1.3.1", "@zjy365/sealos-desktop-sdk": "^0.1.20", "ai": "5.0.51", diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..22fac66 --- /dev/null +++ b/plan.md @@ -0,0 +1,795 @@ +# 移除 Vercel OAuth 登录实施计划 + +> 给执行者:按任务逐项执行。不要开 dev server。改到 TypeScript 或 TSX 文件后,在任务 8 统一运行 `pnpm format`、`pnpm type-check`、`pnpm lint`。本计划只清理 Vercel 登录,不清理和登录无关的 Vercel 功能引用。 + +## 目标 + +把项目的登录能力收敛为只支持 GitHub 登录。删除 Vercel 登录入口、OAuth 路由、Vercel token/session 逻辑和 Vercel SDK,同时保留当前产品仍需要的非登录 Vercel 引用。 + +## 核心原则 + +如无必要,勿增实体。 + +本次不要新增 `lib/session/save.ts`。删除 Vercel 专用 session 代码后,直接复用 `lib/session/create-github.ts` 里已有的 `saveSession()`。 + +## 执行方式 + +本计划会在中间阶段短暂出现 import 指向待删除文件的状态,所以不要在每个任务后要求 `type-check` 通过。按任务顺序完成所有代码和文档修改后,再执行任务 7 的静态搜索和任务 8 的格式、类型、lint 验证。 + +如果执行者想降低中间红线,可以在同一个工作批次内先改 call site,再删对应文件: + +- 先清理所有 `getEnabledAuthProviders` import,再删除 `lib/auth/providers.ts` +- 先把 `saveSession` import 改到 `@/lib/session/create-github`,再删除 `lib/session/create.ts` +- 先清理 GitHub callback 的 connect 分支,再删除 `app/api/auth/github/signin` + +## 必须保留的内容 + +下面这些内容虽然包含 `vercel`,但不是 Vercel 登录代码,不能因为关键词匹配就删除: + +- `app/api/tasks/[taskId]/deployment/route.ts` 里的 Vercel Preview 部署检测 +- `components/icons/vercel-icon.tsx`,如果部署预览 UI 仍在使用它 +- `@vercel/analytics` +- `@vercel/speed-insights` +- AI Gateway 相关引用,例如 `ai-gateway.vercel.sh` +- 旧 runtime 路径兼容,例如 `/home/vercel-sandbox` 和 `/vercel/sandbox` +- `lib/db/migrations/meta/` 里的历史迁移元数据 + +## 必须移除的内容 + +本次要移除这些 Vercel 登录相关能力: + +- Vercel 登录按钮和 loading 状态 +- Vercel OAuth sign-in route +- Vercel OAuth callback route +- sign-out 时撤销 Vercel token 的分支 +- Vercel session 创建和刷新逻辑 +- Vercel teams API route +- Vercel API client helper +- Vercel auth 环境变量 +- `@vercel/sdk` + +## 数据决策 + +第一轮清理不加数据库迁移。 + +原因:`users.provider` 是 text 字段,生产库可能已有 `provider = 'vercel'` 的历史用户。删除 Vercel 登录不应该顺手删除用户,也不应该破坏历史 task 的 user ownership。 + +清理后的预期行为: + +- 旧的 Vercel-only 浏览器 session 会失效,用户需要重新用 GitHub 登录。 +- 以前用 Vercel 登录、并且已经连接过 GitHub 的用户,可以通过 `accounts` 表映射回原来的内部 user id;本次 GitHub 登录拿到的新 token 必须刷新回 `accounts` 表。 +- 以前用 Vercel 登录、但没有连接过 GitHub 的用户,清理后不能直接登录。是否迁移这些用户,需要单独设计一次数据迁移或账号恢复方案。 + +部署前,先在目标数据库执行: + +```sql +select provider, count(*) from users group by provider; + +select count(*) as legacy_vercel_with_github +from users u +join accounts a on a.user_id = u.id +where u.provider = 'vercel' + and a.provider = 'github'; + +select count(*) as legacy_vercel_without_github +from users u +left join accounts a on a.user_id = u.id and a.provider = 'github' +where u.provider = 'vercel' + and a.id is null; + +select provider, external_user_id, count(*) as duplicate_count +from accounts +where provider = 'github' +group by provider, external_user_id +having count(*) > 1; +``` + +如果 `legacy_vercel_without_github` 大于 0,产品侧必须确认可以接受这些用户失去登录入口,或者先写独立迁移方案。 + +如果最后一个重复 GitHub account 查询有结果,必须先确认归属并清理重复行;否则 GitHub 登录通过 `accounts` 映射 legacy 用户时可能无法确定唯一内部用户。 + +--- + +## 任务 1:移除可配置的登录 provider 切换 + +**涉及文件:** + +- 删除:`lib/auth/providers.ts` +- 修改:`components/auth/sign-in.tsx` +- 修改:`components/sealos-home-page-content.tsx` + +### 步骤 + +- [ ] 删除 `lib/auth/providers.ts`。 + +- [ ] 在 `components/auth/sign-in.tsx` 删除这些内容: + - `redirectToSignIn` import + - `getEnabledAuthProviders` import + - `loadingVercel` state + - `hasGitHub` / `hasVercel` 判断 + - `handleVercelSignIn()` + - Vercel 登录按钮 + - Vercel 图标 SVG + - Vercel 相关弹窗文案分支 + +- [ ] 在 `components/auth/sign-in.tsx` 只保留 GitHub popup 登录逻辑: + +```tsx +const [loadingGitHub, setLoadingGitHub] = useState(false) + +const handleGitHubSignIn = async () => { + setLoadingGitHub(true) + try { + await startGitHubPopupAuth('/api/auth/signin/github') + window.location.reload() + } catch (error) { + if (error instanceof GitHubPopupAuthError && error.code === 'popup_blocked') { + toast.error('Please allow popups and try again.') + } else { + toast.error('GitHub authentication failed. Please try again.') + } + setLoadingGitHub(false) + } +} +``` + +- [ ] `components/auth/sign-in.tsx` 的弹窗描述固定为 GitHub: + +```tsx +Sign in with GitHub to continue. +``` + +- [ ] 在 `components/sealos-home-page-content.tsx` 删除这些内容: + - `redirectToSignIn` import + - `getEnabledAuthProviders` import + - `loadingVercel` state + - `hasGitHub` / `hasVercel` 判断 + - `handleVercelSignIn()` + - Vercel 登录按钮 + - Vercel 相关文案分支 + - `handleConnectGitHub()`,如果它只用于 Vercel 登录用户再连接 GitHub 的场景 + +- [ ] 在 `components/sealos-home-page-content.tsx` 的未登录弹窗中,只保留一个 GitHub 登录按钮,点击后调用 `handleGitHubSignIn`。 + +- [ ] `components/sealos-home-page-content.tsx` 的未登录弹窗描述固定为: + +```tsx +You need to sign in with GitHub to create tasks. +``` + +- [ ] 仓库选择区域保留 GitHub-only 的判断: + +```tsx +const canSelectRepository = + isAuthenticated && (githubConnection.connected || isGitHubAuthUser || Boolean(selectedOwner) || Boolean(selectedRepo)) +``` + +- [ ] 如果用户已登录但 GitHub 状态不可用,不要再展示 `Connect GitHub`。展示静态 helper: + +```tsx +
GitHub session unavailable. Sign out and sign in again.
+``` + +- [ ] 不要为了这个状态新增 route、组件或抽象。 + +--- + +## 任务 2:删除 Vercel OAuth route 和 client 代码 + +**涉及文件:** + +- 删除:`app/api/auth/signin/vercel/route.ts` +- 删除:`app/api/auth/callback/vercel/route.ts` +- 删除:`app/api/vercel/teams/route.ts` +- 删除:`lib/vercel-client/user.ts` +- 删除:`lib/vercel-client/teams.ts` +- 删除:`lib/vercel-client/projects.ts` +- 删除:`lib/vercel-client/types.ts` +- 删除:`lib/session/redirect-to-sign-in.ts` +- 删除:`lib/session/create.ts` + +### 步骤 + +- [ ] 删除 Vercel OAuth route 目录: + +```bash +rm -rf app/api/auth/signin/vercel app/api/auth/callback/vercel app/api/vercel +``` + +- [ ] 删除 Vercel client 和 Vercel session helper: + +```bash +rm -rf lib/vercel-client +rm -f lib/session/redirect-to-sign-in.ts lib/session/create.ts +``` + +- [ ] 不要创建 `lib/session/save.ts`。 + +- [ ] 删除后检查是否还有 import 指向已删除文件: + +```bash +rg -n "redirect-to-sign-in|lib/session/create|vercel-client|/api/vercel" app components lib +``` + +预期:无结果。 + +--- + +## 任务 3:把 session 运行时收敛为 GitHub-only + +**涉及文件:** + +- 修改:`lib/session/types.ts` +- 修改:`lib/session/server.ts` +- 修改:`app/api/auth/info/route.ts` +- 修改:`app/api/auth/signout/route.ts` +- 修改:`lib/session/get-oauth-token.ts` +- 修改:`components/auth/user.tsx` + +### 步骤 + +- [ ] 在 `lib/session/types.ts` 中,把 auth provider 类型改成只允许 GitHub: + +```ts +export interface SessionUserInfo { + user: User | undefined + authProvider?: 'github' +} + +export interface Session { + created: number + authProvider: 'github' + user: User +} +``` + +- [ ] 删除 `lib/session/types.ts` 里的 `Tokens` interface,前提是删除 `lib/session/create.ts` 后已经没有 import 使用它。 + +- [ ] 在 `lib/session/server.ts` 中拒绝旧 Vercel session。保留函数名和文件位置: + +```ts +export async function getSessionFromCookie(cookieValue?: string): Promise { + if (!cookieValue) { + return undefined + } + + const decrypted = await decryptJWE(cookieValue) + if (!decrypted || decrypted.authProvider !== 'github') { + return undefined + } + + return { + created: decrypted.created, + authProvider: 'github', + user: decrypted.user, + } +} +``` + +- [ ] 在 `app/api/auth/info/route.ts` 删除这些内容: + - `Tokens` import + - `createSession` import + - `saveSession as saveGitHubSession` alias + - `getOAuthToken` import + - Vercel session refresh 分支 + - 按 provider 分支选择 `saveSession` 的逻辑 + +- [ ] 在 `app/api/auth/info/route.ts` 中,从 `@/lib/session/create-github` import `saveSession`,只使用当前 session: + +```ts +const session = await getSessionFromReq(req) + +const response = new Response(JSON.stringify(await getData(session)), { + headers: { 'Content-Type': 'application/json' }, +}) + +await saveSession(response, session, authCookiePolicy) +return response +``` + +- [ ] 在 `app/api/auth/signout/route.ts` 中,把 `saveSession` import 从 `@/lib/session/create` 改为: + +```ts +import { saveSession } from '@/lib/session/create-github' +``` + +- [ ] 在 `app/api/auth/signout/route.ts` 删除 Vercel token revoke 分支。保留 GitHub session 存在时撤销 GitHub token 的逻辑。 + +- [ ] 在 `lib/session/get-oauth-token.ts` 中,把 provider 类型改为 GitHub-only: + +```ts +type OAuthProvider = 'github' +``` + +- [ ] 在 `lib/session/get-oauth-token.ts` 中删除 Vercel 分支,并更新注释。新注释只描述 GitHub: + - 先查 `accounts` 表里的 connected GitHub account + - 再 fallback 到 `users` 表里的 GitHub primary account + +- [ ] 保持 `getOAuthToken(userId, 'github')` 行为不变。 + +- [ ] 在 `components/auth/user.tsx` 中删除 Vercel fallback。不要再使用: + +```ts +session.authProvider ?? 'vercel' +props.authProvider ?? 'vercel' +``` + +- [ ] 在 `components/auth/user.tsx` 中,用户存在时只传 GitHub provider: + +```tsx +const authProvider = useMemo( + () => (initialized ? session.authProvider : props.authProvider) ?? 'github', + [initialized, session.authProvider, props.authProvider], +) +``` + +或者更直接地,如果 `SignOut` 不再需要 provider 分支,可以把 `SignOut` props 收窄为只接收 `user`,并删除 `authProvider` 传递。优先选择改动更小、和当前组件结构更一致的做法。 + +--- + +## 任务 4:删除仅服务于 Vercel 登录的 GitHub connect/disconnect 流程 + +**涉及文件:** + +- 删除:`app/api/auth/github/signin/route.ts` +- 删除:`app/api/auth/github/disconnect/route.ts` +- 修改:`app/api/auth/github/callback/route.ts` +- 修改:`components/auth/sign-out.tsx` +- 修改:`components/repo-selector.tsx` +- 修改:`components/sealos-home-page-content.tsx` +- 修改:`components/auth/session-provider.tsx` +- 修改:`components/task-sidebar.tsx` +- 修改:`components/task-form.tsx` + +### 步骤 + +- [ ] 删除 GitHub connect account route: + +```bash +rm -rf app/api/auth/github/signin +``` + +- [ ] 删除 GitHub disconnect route: + +```bash +rm -rf app/api/auth/github/disconnect +``` + +- [ ] 在 `app/api/auth/github/callback/route.ts` 中删除 connect flow。删除内容包括: + - `isConnectFlow` + - `github_auth_user_id` cookie 读取和清理 + - `connect` 模式校验 + - connect 分支的账号写入、账号合并事务、`accounts` 写入、`tasks`/`connectors`/`keys` 迁移逻辑 + - 只服务 connect flow 的 imports,例如 `accounts`、`tasks`、`connectors`、`keys`、`eq`、`and`、`inArray`、`encrypt`、`generateId`、`planUserKeyMerge` + +- [ ] `app/api/auth/github/callback/route.ts` 收敛为只接受 sign-in flow。保留 GitHub 登录所需逻辑: + - 校验 popup cookie + - 校验 `github_auth_mode === 'signin'` + - 校验 state + - 用 GitHub code 换 token + - 调用 `createGitHubSession(tokenData.access_token, tokenData.scope)` + - 调用 `saveSession(response, session, authCookiePolicy)` + - 清理 GitHub auth cookies + +- [ ] `app/api/auth/github/callback/route.ts` 中的 `GITHUB_AUTH_COOKIES` 不再包含 `github_auth_user_id`。 + +- [ ] 在 `components/auth/sign-out.tsx` 删除这些内容: + - `githubConnectionAtom` import 和相关 state + - `setGitHubConnection` + - `loadingGitHub` + - `getEnabledAuthProviders` + - `GitHubPopupAuthError` + - `startGitHubPopupAuth` + - `handleGitHubDisconnect()` + - `handleGitHubConnect()` + - `authProvider === 'vercel'` 时展示的 GitHub connect/disconnect 下拉菜单 + - Vercel logout icon 分支 + +- [ ] 在 `components/auth/sign-out.tsx` 中,把 logout 菜单固定为 GitHub 图标: + +```tsx + + + Log Out + +``` + +- [ ] 在 `components/repo-selector.tsx` 中,删除所有对 `/api/auth/github/disconnect` 的请求。 + +- [ ] 在 `components/repo-selector.tsx` 中,如果 GitHub API 返回 `401` 或 `403`,只做本地状态处理: + - 清空 owners cache + - 清空当前 repos cache + - `setGitHubConnection({ connected: false })` + - 停止 loading + - 不调用 disconnect API + +- [ ] 在 `components/repo-selector.tsx` 的本次改动 catch block 中,把动态 console log 改成静态字符串,符合 `AGENTS.md`: + +```ts +console.error('Error loading owners') +console.error('Error loading repos') +console.error('Error verifying external repo') +``` + +- [ ] 保留 `components/auth/session-provider.tsx` 对 `/api/auth/github/status` 的请求。这个 route 会在任务 5 改成真实校验 GitHub token 是否可用,所以这里不要删除该请求。 + +- [ ] 在 `components/auth/session-provider.tsx` 的 catch block 中,把动态 console log 改成静态字符串: + +```ts +console.error('Failed to fetch session') +console.error('Failed to fetch GitHub connection') +``` + +- [ ] 在 `components/task-sidebar.tsx` 中,把 `Connect GitHub to view your repositories` 改成不会暗示还存在 connect flow 的文案: + +```tsx +GitHub session unavailable. Sign out and sign in again. +``` + +- [ ] 在 `components/task-form.tsx` 中,把 `Connect GitHub if needed, choose a repository...` 改成 GitHub-only 登录语义: + +```tsx +Sign in with GitHub, choose a repository, then describe how Sealos should analyze, build, and deploy it. +``` + +--- + +## 任务 5:保留 legacy account mapping,并修正 GitHub token 状态 + +**涉及文件:** + +- 修改:`lib/db/users.ts` +- 修改:`lib/db/schema.ts` +- 修改:`app/api/auth/github/status/route.ts` +- 修改:`lib/session/get-oauth-token.ts` + +### 步骤 + +- [ ] 保留 `upsertUser()` 里第二段检查:如果 GitHub OAuth 身份已经存在于 `accounts` 表,则返回对应 user id。这个逻辑用于兼容历史 Vercel 用户。 + +- [ ] 在 `lib/db/users.ts` 的 legacy `accounts` 命中分支中,除了更新 `users.updatedAt/lastLoginAt`,还必须刷新 `accounts` 表里的 GitHub token 和 profile 字段。否则删除 GitHub connect callback 后,legacy 用户重新 GitHub 登录只能找回 user id,但后续 GitHub API 仍会使用旧 token。 + +实现时使用 `userData.accessToken`。它在 `createGitHubSession()` 调用 `upsertUser()` 前已经被 `encrypt(accessToken)` 加密,不要在 `upsertUser()` 里重复加密。 + +推荐改法: + +```ts +if (existingAccount.length > 0) { + const now = new Date() + + await db.transaction(async (tx) => { + await tx + .update(accounts) + .set({ + accessToken, + scope, + username: userData.username, + updatedAt: now, + }) + .where(and(eq(accounts.provider, 'github'), eq(accounts.externalUserId, externalId))) + + await tx + .update(users) + .set({ + updatedAt: now, + lastLoginAt: now, + }) + .where(eq(users.id, existingAccount[0].userId)) + }) + + return existingAccount[0].userId +} +``` + +不要新增 helper 或 service。这个逻辑属于现有 `upsertUser()` 的 legacy mapping 分支。 + +- [ ] 在 `lib/db/users.ts` 中,把提到 Vercel sign-in 的注释改成 legacy 映射语义: + +```ts +// This preserves legacy users who connected GitHub as a secondary account +// before GitHub became the only supported sign-in method. +``` + +- [ ] 如果没有剩余 caller 需要 Vercel,在 `lib/db/users.ts` 中把 `getUserByExternalId()` 的 provider 类型改成 GitHub-only: + +```ts +export async function getUserByExternalId(provider: 'github', externalId: string) { +``` + +- [ ] 在 `lib/db/schema.ts` 中,第一轮清理先保留 `users.provider` 的 DB enum 范围: + +```ts +enum: ['github', 'vercel'] +``` + +原因:生产库可能已有 `provider = 'vercel'` 的历史行。先删登录能力,不在同一轮做 schema 收窄。 + +- [ ] 在 `lib/db/schema.ts` 中,更新 `accounts` 表附近注释,不再描述“Vercel 用户连接 GitHub”的新流程。改成: + +```ts +// Accounts table - additional OAuth accounts linked to users. +// Kept for legacy users who connected GitHub before GitHub-only sign-in. +``` + +- [ ] 本轮不要增加迁移来移除 `users.provider` 里的 `vercel`。 + +- [ ] 修改 `app/api/auth/github/status/route.ts`。它仍然支持: + - GitHub primary 用户,通过 `users` 表判断 + - legacy 用户,通过 `accounts` 表判断 + +- [ ] `app/api/auth/github/status/route.ts` 不能只查 DB 记录是否存在。它必须在返回 `connected: true` 前校验当前 token 能访问 GitHub。推荐做法: + +```ts +const tokenData = await getOAuthToken(session.user.id, 'github') +if (!tokenData) { + return Response.json({ connected: false }) +} + +const githubResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${tokenData.accessToken}`, + Accept: 'application/vnd.github.v3+json', + }, + cache: 'no-store', +}) + +if (!githubResponse.ok) { + return Response.json({ connected: false }) +} +``` + +- [ ] `app/api/auth/github/status/route.ts` 可以继续用现有 DB 查询返回 `username` 和 `connectedAt`,但只有 token 校验成功后才能返回 `connected: true`。 + +- [ ] `app/api/auth/github/status/route.ts` 不要调用已删除的 disconnect API,也不要新增替代 route。token 不可用时只返回 `{ connected: false }`,UI 会提示用户 sign out/sign in。 + +- [ ] 把 `app/api/auth/github/status/route.ts` 中的动态 console log 改成静态字符串: + +```ts +console.error('Error checking GitHub connection status') +``` + +- [ ] 在 `lib/session/get-oauth-token.ts` 的 catch block 中,把动态 console log 改成静态字符串: + +```ts +console.error('Error fetching OAuth token') +``` + +--- + +## 任务 6:删除 Vercel auth 环境变量和 SDK 依赖 + +**涉及文件:** + +- 修改:`Dockerfile` +- 修改:`README.md` +- 修改:`reference/configuration.md` +- 修改:`AGENTS.md` +- 修改:`package.json` +- 修改:`pnpm-lock.yaml` + +### 步骤 + +- [ ] 在 `Dockerfile` 中删除这些构建参数和环境变量: + +```dockerfile +ARG NEXT_PUBLIC_AUTH_PROVIDERS=github +ARG NEXT_PUBLIC_VERCEL_CLIENT_ID="" +ENV NEXT_PUBLIC_AUTH_PROVIDERS=$NEXT_PUBLIC_AUTH_PROVIDERS +ENV NEXT_PUBLIC_VERCEL_CLIENT_ID=$NEXT_PUBLIC_VERCEL_CLIENT_ID +``` + +- [ ] 在 `README.md` 的 `.env.local` 示例中删除: + +```bash +NEXT_PUBLIC_AUTH_PROVIDERS=github +``` + +- [ ] 在 `README.md` 中确认 auth setup 写的是 GitHub 登录。 + +- [ ] 在 `reference/configuration.md` 中,从 required variables 删除 `NEXT_PUBLIC_AUTH_PROVIDERS`。 + +- [ ] 在 `reference/configuration.md` 中写清楚:当前唯一支持的登录方式是 GitHub OAuth。 + +- [ ] 在 `AGENTS.md` 中更新运行时变量列表: + - 删除 `NEXT_PUBLIC_AUTH_PROVIDERS` + - 删除任何暗示 Vercel OAuth 仍是支持登录 provider 的描述 + - 保留 `GITHUB_CLIENT_ID` + - 保留 `GITHUB_CLIENT_SECRET` + +- [ ] 只移除 `@vercel/sdk`: + +```bash +pnpm remove @vercel/sdk +``` + +- [ ] 确认 `package.json` 里仍保留: + +```json +"@vercel/analytics": "^1.6.1", +"@vercel/speed-insights": "^1.3.1" +``` + +--- + +## 任务 7:静态搜索清理 + +**涉及文件:** + +- 本计划前面改动过的 `app/`、`components/`、`lib/`、文档和配置文件 + +### 步骤 + +- [ ] 执行 Vercel auth 残留搜索: + +```bash +rg -n "signin/vercel|callback/vercel|Sign in with Vercel|Signing in with Vercel|NEXT_PUBLIC_VERCEL_CLIENT_ID|VERCEL_CLIENT_SECRET|loadingVercel|hasVercel|redirect-to-sign-in|vercel-client|/api/vercel" app components lib README.md reference Dockerfile AGENTS.md package.json +``` + +预期:无结果。 + +- [ ] 执行 provider 分支残留搜索: + +```bash +rg -n "authProvider === 'vercel'|authProvider !== 'vercel'|authProvider \\?\\? 'vercel'|provider === 'vercel'|provider: 'vercel'|getEnabledAuthProviders" app components lib +``` + +预期:无结果。注意,如果后续搜索范围扩大到 migration metadata 或历史 schema 快照,可能会看到历史记录,不在本次清理范围内。 + +- [ ] 执行 connect/disconnect 残留搜索: + +```bash +rg -n "/api/auth/github/signin|/api/auth/github/disconnect|handleGitHubConnect|handleGitHubDisconnect|Disconnect GitHub|Connect GitHub|github_auth_user_id|isConnectFlow|github_auth_mode.*connect|planUserKeyMerge" app components lib +``` + +预期:无结果。 + +- [ ] 执行 broad Vercel 搜索: + +```bash +rg -n "vercel" app components lib package.json Dockerfile README.md reference/configuration.md AGENTS.md +``` + +允许剩下的结果只能属于这些类别: + +- Vercel Preview 部署检测 +- Vercel preview UI label 或 icon +- `@vercel/analytics` +- `@vercel/speed-insights` +- AI Gateway URL +- legacy sandbox 路径兼容 +- 有意保留的历史 package/repo 名称 + +- [ ] 如果 broad search 里还有 Vercel auth 或 Vercel login 相关结果,继续删除,不能进入验证阶段。 + +--- + +## 任务 8:格式、类型和 lint 验证 + +**涉及文件:** + +- 所有修改过的 TypeScript 和 TSX 文件 + +### 步骤 + +- [ ] 运行格式化: + +```bash +pnpm format +``` + +预期:exit code 为 0。 + +- [ ] 验证格式: + +```bash +pnpm format:check +``` + +预期:exit code 为 0。 + +- [ ] 运行 TypeScript 检查: + +```bash +pnpm type-check +``` + +预期:exit code 为 0。 + +- [ ] 运行 ESLint: + +```bash +pnpm lint +``` + +预期:exit code 为 0。 + +- [ ] 不要运行这些长驻命令: + - `pnpm dev` + - `npm run dev` + - `next dev` + - `pnpm start` + - 其他会启动 dev server 或长期占用 terminal 的命令 + +--- + +## 任务 9:人工验收 + +这些检查需要在用户或 reviewer 可以运行 app 的环境里执行。 + +- [ ] 打开登录弹窗。 + +预期:只出现 GitHub 登录。 + +- [ ] 直接请求已删除的 Vercel route: + +```bash +curl -i http://localhost:3000/api/auth/signin/vercel +curl -i http://localhost:3000/api/auth/callback/vercel +``` + +预期:返回 `404`。 + +- [ ] 使用 GitHub 登录。 + +预期: + +- 用户头像出现 +- 仓库选择器能加载个人仓库和组织仓库 +- task 创建仍然可用 + +- [ ] 用旧 Vercel session cookie 测试,或者在本地测试 harness 中构造一个解密后为 `authProvider: 'vercel'` 的旧 session。 + +预期: + +- `/api/auth/info` 返回未登录状态 +- session cookie 被清空,或被替换为空 session cookie +- UI 要求用户用 GitHub 登录 + +- [ ] 打开一个已完成且有部署预览的 task。 + +预期: + +- 如果 GitHub checks/deployments 数据里有 Vercel preview URL,Vercel Preview 检测仍然工作 +- 原有的 Vercel Preview UI label/icon 仍然正常渲染 + +--- + +## 完成标准 + +只有全部满足时,才算完成: + +- Vercel 登录 UI 已删除。 +- Vercel OAuth route 已删除。 +- Vercel auth client code 已删除。 +- Vercel session 创建和刷新逻辑已删除。 +- 旧 Vercel session 不再能认证用户。 +- GitHub 登录仍然可用。 +- 已连接 GitHub 的 legacy 用户仍可映射到原内部 user id,并刷新 `accounts` 表里的 GitHub token。 +- `/api/auth/github/status` 只有在当前 GitHub token 可用时才返回 `connected: true`。 +- `@vercel/sdk` 已移除。 +- Analytics、Speed Insights、AI Gateway、部署预览检测、legacy runtime path 兼容都保留。 +- `pnpm format` 通过。 +- `pnpm format:check` 通过。 +- `pnpm type-check` 通过。 +- `pnpm lint` 通过。 +- 静态残留搜索没有发现 Vercel auth 或 login 代码。 + +## 回滚方案 + +如果清理后 GitHub 登录或仓库访问异常: + +1. 只回滚本次清理提交。 +2. 运行: + +```bash +pnpm install --frozen-lockfile +``` + +3. 运行: + +```bash +pnpm type-check +pnpm lint +``` + +4. 不要恢复 Vercel OAuth 环境变量,除非产品明确决定继续支持 Vercel 登录。 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5afe9c9..6dcbde5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,9 +74,6 @@ importers: '@vercel/analytics': specifier: ^1.6.1 version: 1.6.1(next@16.0.10(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) - '@vercel/sdk': - specifier: ^1.18.7 - version: 1.18.7(hono@4.11.5) '@vercel/speed-insights': specifier: ^1.3.1 version: 1.3.1(next@16.0.10(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) @@ -853,12 +850,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1040,16 +1031,6 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@modelcontextprotocol/sdk@1.25.3': - resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -2184,10 +2165,6 @@ packages: vue-router: optional: true - '@vercel/sdk@1.18.7': - resolution: {integrity: sha512-2PwPnlkOEj0H7ieSZW1CPYdqgWBhahlaDpwPLk5RtnAwjB3TqqzQIASBrl9Hj+uZrUeXJR2Lt8A+ONaaYMNAfA==} - hasBin: true - '@vercel/speed-insights@1.3.1': resolution: {integrity: sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==} peerDependencies: @@ -2222,10 +2199,6 @@ packages: peerDependencies: '@kubernetes/client-node': ^0.18.1 - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2242,20 +2215,9 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -2360,10 +2322,6 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2386,10 +2344,6 @@ packages: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2481,32 +2435,12 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -2736,10 +2670,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2875,19 +2805,12 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.277: resolution: {integrity: sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} @@ -2951,9 +2874,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -3081,28 +3001,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3129,9 +3031,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3152,10 +3051,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3195,14 +3090,6 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -3383,20 +3270,12 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} - hono@4.11.5: - resolution: {integrity: sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g==} - engines: {node: '>=16.9.0'} - html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -3410,10 +3289,6 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3430,9 +3305,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -3447,10 +3319,6 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -3534,9 +3402,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3651,12 +3516,6 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -3896,14 +3755,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4035,18 +3886,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4101,10 +3944,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -4181,13 +4020,6 @@ packages: resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} engines: {node: ^10.13.0 || >=12.0.0} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -4226,10 +4058,6 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} @@ -4244,9 +4072,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4279,10 +4104,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -4342,10 +4163,6 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -4357,10 +4174,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.1: - resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} - engines: {node: '>=0.6'} - qs@6.5.5: resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} engines: {node: '>=0.6'} @@ -4368,14 +4181,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - react-dom@19.2.1: resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} peerDependencies: @@ -4495,10 +4300,6 @@ packages: engines: {node: '>= 6'} deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4528,10 +4329,6 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4568,14 +4365,6 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4588,9 +4377,6 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -4657,10 +4443,6 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4772,10 +4554,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - tough-cookie@2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} @@ -4820,10 +4598,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -4900,10 +4674,6 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -4947,17 +4717,13 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -5038,9 +4804,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -5063,11 +4826,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -5571,10 +5329,6 @@ snapshots: reactivity-store: 0.3.12(react@19.2.1) use-sync-external-store: 1.6.0(react@19.2.1) - '@hono/node-server@1.19.9(hono@4.11.5)': - dependencies: - hono: 4.11.5 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -5738,28 +5492,6 @@ snapshots: dependencies: langium: 3.3.1 - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.5)(zod@4.3.6)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.5) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) - transitivePeerDependencies: - - hono - - supports-color - '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 @@ -6878,15 +6610,6 @@ snapshots: next: 16.0.10(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 - '@vercel/sdk@1.18.7(hono@4.11.5)': - dependencies: - '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.5)(zod@4.3.6) - zod: 4.3.6 - transitivePeerDependencies: - - '@cfworker/json-schema' - - hono - - supports-color - '@vercel/speed-insights@1.3.1(next@16.0.10(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)': optionalDependencies: next: 16.0.10(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -6909,11 +6632,6 @@ snapshots: transitivePeerDependencies: - debug - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -6928,10 +6646,6 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ajv-formats@3.0.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6939,13 +6653,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -7075,20 +6782,6 @@ snapshots: before-after-hook@4.0.0: {} - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.14.1 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -7114,8 +6807,6 @@ snapshots: byline@5.0.0: {} - bytes@3.1.2: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -7198,23 +6889,10 @@ snapshots: confbox@0.1.8: {} - content-disposition@1.0.1: {} - - content-type@1.0.5: {} - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - core-util-is@1.0.2: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -7473,8 +7151,6 @@ snapshots: delayed-stream@1.0.0: {} - depd@2.0.0: {} - dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -7525,14 +7201,10 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 - ee-first@1.1.1: {} - electron-to-chromium@1.5.277: {} emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} - enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 @@ -7729,8 +7401,6 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} - escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -7940,51 +7610,8 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: {} - eventsource-parser@3.0.6: {} - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.6 - - express-rate-limit@7.5.1(express@5.2.1): - dependencies: - express: 5.2.1 - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.1 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extend@3.0.2: {} extsprintf@1.3.0: {} @@ -8007,8 +7634,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.0: {} - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8025,17 +7650,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -8079,10 +7693,6 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - forwarded@0.2.0: {} - - fresh@2.0.0: {} - fs-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -8343,20 +7953,10 @@ snapshots: highlight.js@11.11.1: {} - hono@4.11.5: {} - html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - http-signature@1.2.0: dependencies: assert-plus: 1.0.0 @@ -8369,10 +7969,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - ignore@5.3.2: {} ignore@7.0.5: {} @@ -8384,8 +7980,6 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} - inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -8398,8 +7992,6 @@ snapshots: internmap@2.0.3: {} - ipaddr.js@1.9.1: {} - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -8486,8 +8078,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-promise@4.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -8581,10 +8171,6 @@ snapshots: json-schema-traverse@0.4.1: {} - json-schema-traverse@1.0.0: {} - - json-schema-typed@8.0.2: {} - json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -8911,10 +8497,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - merge2@1.4.1: {} mermaid@11.12.2: @@ -9180,16 +8762,10 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -9235,8 +8811,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@1.0.0: {} - next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: react: 19.2.1 @@ -9320,14 +8894,6 @@ snapshots: oidc-token-hash@5.2.0: optional: true - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -9387,8 +8953,6 @@ snapshots: dependencies: entities: 6.0.1 - parseurl@1.3.3: {} - path-data-parser@0.1.0: {} path-exists@4.0.0: {} @@ -9397,8 +8961,6 @@ snapshots: path-parse@1.0.7: {} - path-to-regexp@8.3.0: {} - pathe@2.0.3: {} performance-now@2.1.0: {} @@ -9425,8 +8987,6 @@ snapshots: picomatch@4.0.3: {} - pkce-challenge@5.0.1: {} - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -9480,11 +9040,6 @@ snapshots: property-information@7.1.0: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - proxy-from-env@2.1.0: {} psl@1.15.0: @@ -9493,23 +9048,10 @@ snapshots: punycode@2.3.1: {} - qs@6.14.1: - dependencies: - side-channel: 1.1.0 - qs@6.5.5: {} queue-microtask@1.2.3: {} - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - react-dom@19.2.1(react@19.2.1): dependencies: react: 19.2.1 @@ -9696,8 +9238,6 @@ snapshots: tunnel-agent: 0.6.0 uuid: 3.4.0 - require-from-string@2.0.2: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -9727,16 +9267,6 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -9772,31 +9302,6 @@ snapshots: semver@7.7.3: {} - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9819,8 +9324,6 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - setprototypeof@1.2.0: {} - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -9933,8 +9436,6 @@ snapshots: state-local@1.0.7: {} - statuses@2.0.2: {} - stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -10088,8 +9589,6 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: {} - tough-cookie@2.5.0: dependencies: psl: 1.15.0 @@ -10133,12 +9632,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -10250,8 +9743,6 @@ snapshots: universal-user-agent@7.0.3: {} - unpipe@1.0.0: {} - unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -10311,8 +9802,6 @@ snapshots: uuid@9.0.1: {} - vary@1.1.2: {} - vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -10418,8 +9907,6 @@ snapshots: word-wrap@1.2.5: {} - wrappy@1.0.2: {} - ws@8.19.0: {} yallist@3.1.1: {} @@ -10428,10 +9915,6 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.25.1(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/reference/configuration.md b/reference/configuration.md index 6deb65a..9147906 100644 --- a/reference/configuration.md +++ b/reference/configuration.md @@ -13,7 +13,6 @@ These values are required for the app to boot and run its main task flow: - `DEVBOX_TOKEN`: static Devbox API token - `JWE_SECRET`: secret for session encryption - `ENCRYPTION_KEY`: symmetric key used to encrypt stored tokens and user API keys -- `NEXT_PUBLIC_AUTH_PROVIDERS`: should include `github` for the current primary flow - `GITHUB_CLIENT_ID`: GitHub OAuth client ID - `GITHUB_CLIENT_SECRET`: GitHub OAuth client secret @@ -49,7 +48,7 @@ For a host such as `staging-usw-1.sealos.io`, the app derives: ### GitHub OAuth -The current primary sign-in flow is GitHub-based. +The only supported sign-in flow is GitHub OAuth. Required OAuth scopes are defined in `lib/auth/oauth.ts`: