import { randomBytes } from "node:crypto"; import { spawn } from "node:child_process"; import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import { db, getSetting } from "./db.mjs"; import { json, readBody, redirect } from "./http.mjs"; import { getRequestSession } from "./auth.mjs"; import { createSession, sessionCookie, updateSession } from "./session.mjs"; const oauthStates = new Map(); const githubRequestTimeoutMs = Number(process.env.GITHUB_HTTP_TIMEOUT_MS || 60000); const githubUserAgent = "VitePress-CMS"; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isRetriableNetworkError(error) { return ["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENOTFOUND", "ECONNREFUSED"].includes(error?.code); } function canUsePowerShellFallback(targetUrl) { const url = new URL(targetUrl); return process.platform === "win32" && url.hostname === "github.com"; } function requestTextWithPowerShell(targetUrl, init = {}) { return new Promise((resolve, reject) => { const payload = Buffer.from( JSON.stringify({ url: targetUrl, method: init.method || "GET", headers: init.headers || {}, body: init.body || "", }), "utf-8", ).toString("base64"); const script = ` $ErrorActionPreference = 'Stop' $payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($args[0])) | ConvertFrom-Json $headers = @{} if ($payload.headers) { foreach ($property in $payload.headers.PSObject.Properties) { $headers[$property.Name] = [string]$property.Value } } $params = @{ Uri = [string]$payload.url Method = [string]$payload.method Headers = $headers TimeoutSec = 180 UseBasicParsing = $true } if ($payload.body -ne $null -and [string]$payload.body -ne '') { $params.Body = [string]$payload.body } try { $response = Invoke-WebRequest @params $result = @{ ok = ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300) status = [int]$response.StatusCode headers = @{} text = [string]$response.Content } } catch { $webResponse = $_.Exception.Response if ($webResponse -eq $null) { throw } $stream = $webResponse.GetResponseStream() $reader = New-Object IO.StreamReader($stream) $text = $reader.ReadToEnd() $status = [int]$webResponse.StatusCode $result = @{ ok = ($status -ge 200 -and $status -lt 300) status = $status headers = @{} text = [string]$text } } $result | ConvertTo-Json -Compress -Depth 5 `; const encodedCommand = Buffer.from(script, "utf16le").toString("base64"); const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand, payload], { windowsHide: true, }); const stdout = []; const stderr = []; const timeout = setTimeout(() => { child.kill(); reject(new Error("PowerShell GitHub request timed out")); }, 190000); child.stdout.on("data", (chunk) => stdout.push(chunk)); child.stderr.on("data", (chunk) => stderr.push(chunk)); child.on("error", (error) => { clearTimeout(timeout); reject(error); }); child.on("close", (code) => { clearTimeout(timeout); if (code !== 0) { reject(new Error(Buffer.concat(stderr).toString("utf-8") || `PowerShell exited with ${code}`)); return; } try { resolve(JSON.parse(Buffer.concat(stdout).toString("utf-8"))); } catch (error) { reject(error); } }); }); } function requestText(targetUrl, init = {}) { return new Promise((resolve, reject) => { const url = new URL(targetUrl); const transport = url.protocol === "http:" ? httpRequest : httpsRequest; const headers = { ...init.headers }; const body = init.body; if (typeof body === "string" && !headers["Content-Length"]) { headers["Content-Length"] = Buffer.byteLength(body); } const request = transport( url, { method: init.method || "GET", headers, timeout: githubRequestTimeoutMs, }, (response) => { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { resolve({ ok: response.statusCode >= 200 && response.statusCode < 300, status: response.statusCode, headers: response.headers, text: Buffer.concat(chunks).toString("utf-8"), }); }); }, ); request.on("timeout", () => { request.destroy(new Error(`GitHub request timed out after ${githubRequestTimeoutMs}ms`)); }); request.on("error", reject); if (body) request.write(body); request.end(); }); } async function requestJson(targetUrl, init = {}) { let response; for (let attempt = 1; attempt <= 3; attempt += 1) { try { response = await requestText(targetUrl, init); break; } catch (error) { if (canUsePowerShellFallback(targetUrl) && isRetriableNetworkError(error)) { response = await requestTextWithPowerShell(targetUrl, init); break; } if (attempt === 3 || !isRetriableNetworkError(error)) { throw error; } await sleep(1000 * attempt); } } let payload = null; if (response?.text) { try { payload = JSON.parse(response.text); } catch { payload = null; } } return { ...response, payload }; } function getBaseUrl(request) { const configuredBaseUrl = process.env.GITHUB_OAUTH_REDIRECT_BASE_URL; if (configuredBaseUrl) return configuredBaseUrl.replace(/\/$/, ""); const host = request.headers.host ?? "localhost:5173"; const protocol = host.startsWith("localhost") || host.startsWith("127.0.0.1") ? "http" : "https"; return `${protocol}://${host}`; } function trimTrailingSlash(value) { return value.replace(/\/$/, ""); } function getSettingOrFallback(key, fallback = "") { const value = getSetting(key, ""); return value || fallback; } function getGithubConfig() { return { apiBaseUrl: trimTrailingSlash(getSettingOrFallback("github.api_base_url", process.env.GITHUB_API_BASE_URL || "https://api.github.com")), webBaseUrl: trimTrailingSlash(getSettingOrFallback("github.web_base_url", process.env.GITHUB_WEB_BASE_URL || "https://github.com")), oauthEnabled: getSettingOrFallback("auth.allow_github_login", "true") === "true", clientId: getSettingOrFallback("github.oauth_client_id", process.env.GITHUB_CLIENT_ID || ""), clientSecret: getSettingOrFallback("github.oauth_client_secret", process.env.GITHUB_CLIENT_SECRET || ""), }; } function getOAuthConfig(request) { const config = getGithubConfig(); const baseUrl = getBaseUrl(request); if (!config.oauthEnabled) { throw new Error("GitHub login is disabled in system settings"); } if (!config.clientId || !config.clientSecret) { throw new Error("请先配置 GITHUB_CLIENT_ID 和 GITHUB_CLIENT_SECRET"); } return { ...config, redirectUri: `${baseUrl}/api/github/callback`, }; } async function githubFetch(session, path, init = {}) { const { apiBaseUrl } = getGithubConfig(); const response = await requestJson(`${apiBaseUrl}${path}`, { ...init, headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${session.accessToken}`, "User-Agent": githubUserAgent, "X-GitHub-Api-Version": "2022-11-28", ...init.headers, }, }); if (!response.ok) { const details = response.text; const error = new Error(`GitHub API error ${response.status}: ${details}`); error.statusCode = response.status; throw error; } return response.payload; } function requireSession(request, response) { const { session } = getRequestSession(request); if (!session?.github?.accessToken) { json(response, 401, { error: "Not signed in" }); return undefined; } return { ...session, accessToken: session.github.accessToken, }; } function publicCmsUser(user) { return { id: user.id, email: user.email, name: user.name, avatarUrl: user.avatar_url, role: user.role, }; } function findOrCreateGithubUser(githubUser, accessToken, currentSession) { const providerUserId = String(githubUser.id); const linkedAccount = db .prepare("SELECT user_id FROM oauth_accounts WHERE provider = 'github' AND provider_user_id = ?") .get(providerUserId); let user; if (linkedAccount) { user = db.prepare("SELECT * FROM users WHERE id = ?").get(linkedAccount.user_id); } else if (currentSession?.user?.id) { user = db.prepare("SELECT * FROM users WHERE id = ?").get(currentSession.user.id); } else { const email = githubUser.email ? String(githubUser.email).toLowerCase() : null; const existingByEmail = email ? db.prepare("SELECT * FROM users WHERE email = ?").get(email) : null; if (existingByEmail) { user = existingByEmail; } else { const result = db .prepare("INSERT INTO users (email, name, avatar_url, role, email_verified) VALUES (?, ?, ?, 'user', ?)") .run(email, githubUser.name || githubUser.login, githubUser.avatar_url, email ? 1 : 0); user = db.prepare("SELECT * FROM users WHERE id = ?").get(result.lastInsertRowid); } } db.prepare(` INSERT INTO oauth_accounts (user_id, provider, provider_user_id, provider_login, access_token, avatar_url, updated_at) VALUES (?, 'github', ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(provider, provider_user_id) DO UPDATE SET user_id = excluded.user_id, provider_login = excluded.provider_login, access_token = excluded.access_token, avatar_url = excluded.avatar_url, updated_at = CURRENT_TIMESTAMP `).run(user.id, providerUserId, githubUser.login, accessToken, githubUser.avatar_url); db.prepare("UPDATE users SET avatar_url = COALESCE(avatar_url, ?), updated_at = CURRENT_TIMESTAMP WHERE id = ?").run( githubUser.avatar_url, user.id, ); return db.prepare("SELECT * FROM users WHERE id = ?").get(user.id); } async function handleLogin(request, response) { const { clientId, redirectUri, webBaseUrl } = getOAuthConfig(request); const state = randomBytes(16).toString("hex"); oauthStates.set(state, Date.now()); const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, scope: "repo workflow", state, }); redirect(response, `${webBaseUrl}/login/oauth/authorize?${params.toString()}`); } async function handleCallback(request, response, url) { const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state || !oauthStates.has(state)) { redirect(response, "/?github=oauth_error"); return; } oauthStates.delete(state); const { clientId, clientSecret, redirectUri, apiBaseUrl, webBaseUrl } = getOAuthConfig(request); const tokenResponse = await requestJson(`${webBaseUrl}/login/oauth/access_token`, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "User-Agent": githubUserAgent, }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code, redirect_uri: redirectUri, }), }); const tokenPayload = tokenResponse.payload ?? {}; if (!tokenPayload.access_token) { redirect(response, "/?github=token_error"); return; } const userResponse = await requestJson(`${apiBaseUrl}/user`, { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${tokenPayload.access_token}`, "User-Agent": githubUserAgent, "X-GitHub-Api-Version": "2022-11-28", }, }); if (!userResponse.ok) { throw new Error(`GitHub user API error ${userResponse.status}: ${userResponse.text}`); } const user = userResponse.payload ?? {}; const { session: currentSession } = getRequestSession(request); const cmsUser = findOrCreateGithubUser(user, tokenPayload.access_token, currentSession); const sessionId = createSession({ user: publicCmsUser(cmsUser), github: { accessToken: tokenPayload.access_token, login: user.login, avatarUrl: user.avatar_url, name: user.name, }, }); redirect(response, "/?github=connected", { "Set-Cookie": sessionCookie(sessionId), }); } async function handleMe(request, response) { const { session } = getRequestSession(request); if (!session) { json(response, 200, { user: null }); return; } json(response, 200, { user: session.github ?? null }); } async function handleLogout(request, response) { const { sessionId, session } = getRequestSession(request); if (session) { const { github, ...restSession } = session; updateSession(sessionId, restSession); } json(response, 200, { ok: true }); } async function handleRepos(request, response) { const session = requireSession(request, response); if (!session) return; const repos = await githubFetch( session, "/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member", ); json(response, 200, { repos: repos.map((repo) => ({ id: repo.id, fullName: repo.full_name, owner: repo.owner.login, name: repo.name, private: repo.private, defaultBranch: repo.default_branch, updatedAt: repo.updated_at, })), }); } async function handleCreateRepo(request, response) { const session = requireSession(request, response); if (!session) return; const body = JSON.parse(await readBody(request) || "{}"); if (!body.name) { json(response, 400, { error: "Repository name is required" }); return; } const repo = await githubFetch(session, "/user/repos", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: body.name, private: Boolean(body.private), auto_init: true, description: body.description || "Created by VitePress-CMS", }), }); json(response, 201, { repo: { id: repo.id, fullName: repo.full_name, owner: repo.owner.login, name: repo.name, private: repo.private, defaultBranch: repo.default_branch, updatedAt: repo.updated_at, }, }); } function normalizeSiteRoot(siteRoot = "") { return siteRoot.trim().replace(/^\/+|\/+$/g, ""); } function toRepoPath(siteRoot, path) { const normalizedRoot = normalizeSiteRoot(siteRoot); return normalizedRoot ? `${normalizedRoot}/${path}` : path; } function fromRepoPath(siteRoot, path) { const normalizedRoot = normalizeSiteRoot(siteRoot); return normalizedRoot && path.startsWith(`${normalizedRoot}/`) ? path.slice(normalizedRoot.length + 1) : path; } function decodeBase64Content(content) { return Buffer.from(content.replace(/\n/g, ""), "base64").toString("utf-8"); } function encodeBase64Content(content) { return Buffer.from(content, "utf-8").toString("base64"); } function packageJsonTemplate(siteRoot) { const root = normalizeSiteRoot(siteRoot) || "."; return `${JSON.stringify( { scripts: { "docs:dev": `vitepress dev ${root}`, "docs:build": `vitepress build ${root}`, "docs:preview": `vitepress preview ${root}`, }, devDependencies: { vitepress: "^1.6.4", }, }, null, 2, )} `; } function siteConfigTemplate() { return `import { defineConfig } from "vitepress"; export default defineConfig({ title: "VitePress Site", description: "Created by VitePress-CMS", lastUpdated: true, themeConfig: { nav: [ { text: "首页", link: "/" }, { text: "指南", link: "/guide/getting-started" }, ], sidebar: { "/guide/": [ { text: "指南", items: [ { text: "快速开始", link: "/guide/getting-started" }, ], }, ], }, search: { provider: "local", }, footer: { message: "Powered by VitePress-CMS", copyright: "Copyright 2026", }, }, }); `; } function deployWorkflowTemplate(siteRoot, branch) { const root = normalizeSiteRoot(siteRoot); const outputDir = root ? `${root}/.vitepress/dist` : ".vitepress/dist"; return `name: Deploy VitePress site to Pages on: push: branches: [${branch || "main"}] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm install - run: npm run docs:build - uses: actions/upload-pages-artifact@v3 with: path: ${outputDir} deploy: environment: name: github-pages url: \${{ steps.deployment.outputs.page_url }} needs: build runs-on: ubuntu-latest steps: - id: deployment uses: actions/deploy-pages@v4 `; } function initTemplateFiles(siteRoot, branch) { return [ { path: "package.json", content: packageJsonTemplate(siteRoot), }, { path: toRepoPath(siteRoot, ".vitepress/config.ts"), content: siteConfigTemplate(), }, { path: toRepoPath(siteRoot, "index.md"), content: `--- title: 首页 description: VitePress-CMS 初始化页面 --- # VitePress Site 这个站点由 VitePress-CMS 初始化。 `, }, { path: toRepoPath(siteRoot, "guide/getting-started.md"), content: `--- title: 快速开始 description: 使用 VitePress-CMS 管理内容 --- # 快速开始 这里是你的第一篇指南文档。 `, }, { path: ".github/workflows/deploy.yml", content: deployWorkflowTemplate(siteRoot, branch), }, ]; } function parseFrontmatter(source) { const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); if (!match) { return { title: "", description: "", content: source, }; } const frontmatter = match[1]; const title = frontmatter.match(/^title:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? ""; const description = frontmatter.match(/^description:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? ""; return { title, description, content: source.slice(match[0].length), }; } function stringifyMarkdownPage(page) { return [ "---", `title: ${page.title || "未命名页面"}`, `description: ${page.description || ""}`, "---", "", page.content.trimStart(), ].join("\n"); } function readStringValue(source, key, fallback = "") { const pattern = new RegExp(`${key}:\\s*["'\`]([^"'\`]+)["'\`]`); return source.match(pattern)?.[1] ?? fallback; } function readBooleanValue(source, key, fallback = false) { const pattern = new RegExp(`${key}:\\s*(true|false)`); const value = source.match(pattern)?.[1]; return value ? value === "true" : fallback; } function parseSiteSettings(source) { const socialLinksSource = source.match(/socialLinks:\s*\[([\s\S]*?)\]/)?.[1] ?? ""; return { title: readStringValue(source, "title", "VitePress Site"), description: readStringValue(source, "description"), logo: readStringValue(source, "logo", "/logo.svg"), lastUpdated: readBooleanValue(source, "lastUpdated", true), localSearch: /search:\s*{[\s\S]*?provider:\s*["']local["'][\s\S]*?}/.test(source), outline: readBooleanValue(source, "outline", true), socialKind: readStringValue(socialLinksSource, "icon", "github"), socialLink: readStringValue(socialLinksSource, "link", "https://github.com/example/vitepress-cms"), }; } function stringifySiteConfig(settings) { const searchConfig = settings.localSearch ? ` search: { provider: "local", },` : ""; return `import { defineConfig } from "vitepress"; export default defineConfig({ title: ${JSON.stringify(settings.title)}, description: ${JSON.stringify(settings.description)}, lastUpdated: ${settings.lastUpdated}, themeConfig: { logo: ${JSON.stringify(settings.logo)}, outline: ${settings.outline}, nav: [ { text: "首页", link: "/" }, { text: "指南", link: "/guide/getting-started" }, { text: "配置", link: "/guide/config" }, { text: "更新日志", link: "/changelog" }, ], sidebar: { "/guide/": [ { text: "指南", items: [ { text: "快速开始", link: "/guide/getting-started" }, { text: "页面管理", link: "/guide/pages" }, { text: "配置说明", link: "/guide/config" }, ], }, ], }, socialLinks: [ { icon: ${JSON.stringify(settings.socialKind)}, link: ${JSON.stringify(settings.socialLink)} }, ], footer: { message: "Powered by VitePress-CMS", copyright: "Copyright 2026", }, ${searchConfig} }, }); `; } function getSiteQuery(url) { const owner = url.searchParams.get("owner"); const repo = url.searchParams.get("repo"); const branch = url.searchParams.get("branch") || "main"; const siteRoot = url.searchParams.get("siteRoot") || ""; if (!owner || !repo) { throw new Error("owner and repo are required"); } return { owner, repo, branch, siteRoot }; } async function getRepoContent(session, site, path) { const repoPath = toRepoPath(site.siteRoot, path); const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/"); return githubFetch( session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}?ref=${encodeURIComponent(site.branch)}`, ); } async function getRepoPathContent(session, site, repoPath) { const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/"); return githubFetch( session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}?ref=${encodeURIComponent(site.branch)}`, ); } async function createRepoPathFile(session, site, file) { try { const existing = await getRepoPathContent(session, site, file.path); if (file.path.startsWith(".github/workflows/")) { const encodedPath = file.path.split("/").map(encodeURIComponent).join("/"); const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: `docs: update ${file.path} from VitePress-CMS`, content: encodeBase64Content(file.content), branch: site.branch, sha: existing.sha, }), }); return { path: file.path, status: "updated", sha: result.content.sha, }; } return { path: file.path, status: "skipped", }; } catch (error) { if (file.path.startsWith(".github/workflows/") && error?.statusCode === 403) { return { path: file.path, status: "failed", error: error.message, }; } if (error?.statusCode !== 404) { throw error; } } const encodedPath = file.path.split("/").map(encodeURIComponent).join("/"); let result; try { result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: `docs: initialize ${file.path} from VitePress-CMS`, content: encodeBase64Content(file.content), branch: site.branch, }), }); } catch (error) { if (file.path.startsWith(".github/workflows/") && [403, 404].includes(error?.statusCode)) { return { path: file.path, status: "failed", error: error.message, }; } if (isRetriableNetworkError(error) || error?.statusCode === 422) { const existing = await getRepoPathContent(session, site, file.path).catch(() => null); if (existing) { return { path: file.path, status: "skipped", sha: existing.sha, }; } } throw error; } return { path: file.path, status: "created", sha: result.content.sha, }; } async function handleInitSite(request, response) { const session = requireSession(request, response); if (!session) return; const body = JSON.parse(await readBody(request) || "{}"); const site = body.site; if (!site?.owner || !site?.repo) { json(response, 400, { error: "owner and repo are required" }); return; } const normalizedSite = { owner: String(site.owner), repo: String(site.repo), branch: String(site.branch || "main"), siteRoot: normalizeSiteRoot(String(site.siteRoot || "")), }; const files = []; for (const file of initTemplateFiles(normalizedSite.siteRoot, normalizedSite.branch)) { files.push(await createRepoPathFile(session, normalizedSite, file)); } json(response, 200, { files }); } async function handleSitePages(request, response, url) { const session = requireSession(request, response); if (!session) return; if (request.method === "GET") { const site = getSiteQuery(url); const tree = await githubFetch( session, `/repos/${site.owner}/${site.repo}/git/trees/${encodeURIComponent(site.branch)}?recursive=1`, ); const siteRoot = normalizeSiteRoot(site.siteRoot); const prefix = siteRoot ? `${siteRoot}/` : ""; const markdownFiles = tree.tree .filter((entry) => entry.type === "blob") .filter((entry) => entry.path.endsWith(".md")) .filter((entry) => !entry.path.includes("/node_modules/")) .filter((entry) => entry.path !== `${prefix}README.md`) .filter((entry) => !prefix || entry.path.startsWith(prefix)); const pages = await Promise.all( markdownFiles.sort((a, b) => a.path.localeCompare(b.path)).map(async (entry) => { const file = await getRepoContent(session, site, fromRepoPath(site.siteRoot, entry.path)); const source = decodeBase64Content(file.content); const parsed = parseFrontmatter(source); const path = fromRepoPath(site.siteRoot, entry.path); return { id: path, path, sha: file.sha, title: parsed.title || path.replace(/\.md$/, ""), description: parsed.description, content: parsed.content, }; }), ); json(response, 200, { pages }); return; } if (request.method === "PUT") { const body = JSON.parse(await readBody(request) || "{}"); const site = body.site; const page = body.page; const repoPath = toRepoPath(site.siteRoot, page.path); const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/"); const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: `docs: update ${page.path} from VitePress-CMS`, content: encodeBase64Content(stringifyMarkdownPage(page)), branch: site.branch, sha: page.sha, }), }); json(response, 200, { sha: result.content.sha }); return; } json(response, 405, { error: "Method not allowed" }); } async function handleSiteSettings(request, response, url) { const session = requireSession(request, response); if (!session) return; if (request.method === "GET") { const site = getSiteQuery(url); const file = await getRepoContent(session, site, ".vitepress/config.ts"); json(response, 200, { settings: parseSiteSettings(decodeBase64Content(file.content)), sha: file.sha, }); return; } if (request.method === "PUT") { const body = JSON.parse(await readBody(request) || "{}"); const site = body.site; const settings = body.settings; const repoPath = toRepoPath(site.siteRoot, ".vitepress/config.ts"); const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/"); const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: "docs: update VitePress config from VitePress-CMS", content: encodeBase64Content(stringifySiteConfig(settings)), branch: site.branch, sha: body.sha, }), }); json(response, 200, { sha: result.content.sha }); return; } json(response, 405, { error: "Method not allowed" }); } export async function handleGithubApi(request, response, url) { try { if (url.pathname === "/api/github/login" && request.method === "GET") { await handleLogin(request, response); return true; } if (url.pathname === "/api/github/callback" && request.method === "GET") { await handleCallback(request, response, url); return true; } if (url.pathname === "/api/github/me" && request.method === "GET") { await handleMe(request, response); return true; } if (url.pathname === "/api/github/logout" && request.method === "POST") { await handleLogout(request, response); return true; } if (url.pathname === "/api/github/repos" && request.method === "GET") { await handleRepos(request, response); return true; } if (url.pathname === "/api/github/repos" && request.method === "POST") { await handleCreateRepo(request, response); return true; } if (url.pathname === "/api/github/site/init" && request.method === "POST") { await handleInitSite(request, response); return true; } if (url.pathname === "/api/github/site/pages") { await handleSitePages(request, response, url); return true; } if (url.pathname === "/api/github/site/settings") { await handleSiteSettings(request, response, url); return true; } return false; } catch (error) { json(response, error?.statusCode || 500, { error: error instanceof Error ? error.message : "Unknown GitHub API error", }); return true; } }