import { ensureAccess } from './cloudflare-access.ts'; import type { CloudflareApi } from './cloudflare-api.ts'; import type { DeploymentSettings, RuntimeSecrets } from './deployment-config.ts'; interface AuthenticationOptions { existingSecrets?: Set; fixedGithubAdminId?: string; previousProvider?: DeploymentSettings['authProvider']; fetcher?: typeof fetch; } export function requiredAuthSecrets(provider: DeploymentSettings['authProvider']): string[] { return provider === 'github' ? ['GITHUB_CLIENT_SECRET'] : ['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']; } export async function prepareAuthentication( api: CloudflareApi, settings: DeploymentSettings, hostname: string, options: AuthenticationOptions = {}, ): Promise<{ secrets: Partial; githubAdminId?: string }> { const fetcher = options.fetcher ?? fetch; if (settings.authProvider === 'cloudflare') { const canRetain = options.previousProvider === 'cloudflare' && requiredAuthSecrets('cloudflare').every((name) => options.existingSecrets?.has(name)); if (canRetain) { try { const response = await fetcher(`https://${hostname}/api/auth/me`, { redirect: 'manual', signal: AbortSignal.timeout(30_000), }); const location = response.headers.get('Location'); const target = location ? new URL(location, `https://${hostname}`) : null; if (target && (target.hostname.endsWith('.cloudflareaccess.com') || target.pathname.startsWith('/cdn-cgi/access/'))) { console.log('已确认当前入口仍由 Cloudflare Access 保护,保留现有认证 Secret。'); return { secrets: {} }; } } catch { throw new Error('无法确认当前入口的 Cloudflare Access 登录是否可用,请检查域名后重试。'); } } // 首次启用、切回 Access 或现有配置不完整时,必须从对应应用刷新配置。 return { secrets: await ensureAccess(api, settings, hostname) }; } // GitHub 新部署完全不调用 Zero Trust API。旧域名若仍在 Access 后面,明确停止, // 不擅自删除用户的安全配置,也不把“双重登录”误报为部署成功。 try { const response = await fetcher(`https://${hostname}/api/auth/me`, { redirect: 'manual', signal: AbortSignal.timeout(30_000) }); const location = response.headers.get('Location'); const target = location ? new URL(location, `https://${hostname}`) : null; if (target && (target.hostname.endsWith('.cloudflareaccess.com') || target.pathname.startsWith('/cdn-cgi/access/'))) { throw new Error('当前域名仍由 Cloudflare Access 保护。请先在 Access 解除该域名的保护,再切换 GitHub;不会删除主机资料或加密密钥。'); } } catch (error) { if (error instanceof Error && error.message.includes('仍由 Cloudflare Access 保护')) throw error; if (options.previousProvider === 'cloudflare' || [...(options.existingSecrets ?? [])].some((name) => name.startsWith('ACCESS_'))) { throw new Error('无法确认入口是否已解除 Cloudflare Access 保护,请检查域名后重试;不会自动删除现有策略。'); } } const fixed = settings.githubAdminId ?? options.fixedGithubAdminId; if (fixed) { return { githubAdminId: fixed, secrets: { GITHUB_CLIENT_SECRET: settings.secrets.GITHUB_CLIENT_SECRET! } }; } if (!settings.githubAdmin) { throw new Error('首次配置 GitHub 登录需要 GITHUB_ADMIN 用户名,或显式设置 GITHUB_ADMIN_ID。'); } const response = await fetcher(`https://api.github.com/users/${settings.githubAdmin}`, { headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'EdgeSSH' }, signal: AbortSignal.timeout(30_000), }); if (!response.ok) throw new Error(`无法确认 GITHUB_ADMIN(HTTP ${response.status}),请检查用户名或稍后重试。`); const user = await response.json() as { id: number; type: string }; if (user.type !== 'User' || !Number.isSafeInteger(user.id) || user.id <= 0) { throw new Error('GITHUB_ADMIN 必须是个人 GitHub 账号,不能是组织。'); } return { githubAdminId: String(user.id), secrets: { GITHUB_CLIENT_SECRET: settings.secrets.GITHUB_CLIENT_SECRET! } }; }