/* * ════════════════════════════════════════════════════════════════ * 光湖 · 密钥授权路由 · 人格体请求→用户授权→自动入库 * Sovereign: TCS-0002∞ · ICE-GL∞ · 国作登字-2026-A-00037559 * 守护: 铸渊 · ICE-GL-ZY001 * ════════════════════════════════════════════════════════════════ * * 设计理念 (冰朔 2026-05-12 提出的需求): * * 1. 人格体需要密钥 → 弹出授权页 * 2. 用户点"授权" → 人格体随机生成 Token → 自动保存到 vault * 3. 用户能看到所有密钥(在 vault 管理页)· AI 看不到明文 * 4. 系统只调用权限,不暴露 Token * 5. 用户随时删/重新生成/自己设 * * Gitea OAuth2 集成: * 用户在 Gitea 上点授权 → OAuth2 回调 → 自动生成 Gitea Token → 存入 vault * 铸渊后续通过 vault 的 /internal/fetch/:key 拿 Token(本机-only,AI 不窥视明文) * * 路由: * GET /admin/auth/pending 列出待授权请求 * POST /admin/auth/request 人格体发起授权请求 * POST /admin/auth/:id/approve 用户批准 → 生成Token → 存入vault * POST /admin/auth/:id/reject 用户拒绝 * GET /admin/auth/gitea/authorize 跳转到 Gitea OAuth2 授权页 * GET /admin/auth/gitea/callback OAuth2 回调 → 自动处理 * * cc-004: 全部中文回执 */ "use strict"; const express = require("express"); const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); // ─── 待授权请求存储 (内存 + 文件持久化) ─────────────────────── class AuthRequestStore { constructor(storePath) { this.storePath = storePath; this.requests = new Map(); this._load(); } _load() { try { if (fs.existsSync(this.storePath)) { const data = JSON.parse(fs.readFileSync(this.storePath, "utf-8")); for (const r of data.requests || []) { this.requests.set(r.id, r); } } } catch (_) {} } _save() { const dir = path.dirname(this.storePath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(this.storePath, JSON.stringify({ _sovereign: "TCS-0002∞ · 国作登字-2026-A-00037559", requests: Array.from(this.requests.values()) }, null, 2)); } add(request) { this.requests.set(request.id, request); this._save(); return request; } get(id) { return this.requests.get(id) || null; } update(id, updates) { const req = this.requests.get(id); if (!req) return null; Object.assign(req, updates); this._save(); return req; } list() { return Array.from(this.requests.values()); } listPending() { return this.list().filter(r => r.status === "pending"); } } // ─── Token 生成 ─────────────────────────────────────────────── function generateToken(prefix = "gmp") { const bytes = crypto.randomBytes(32); return `${prefix}_${bytes.toString("base64url").slice(0, 40)}`; } function generateGiteaToken() { // Gitea Token 格式: gto_ + 随机字符串 const bytes = crypto.randomBytes(24); return `gto_${bytes.toString("base64url")}`; } // ─── 构建路由 ───────────────────────────────────────────────── function buildAuthRouter({ vault, storePath, giteaOAuth }) { const router = express.Router(); const store = new AuthRequestStore(storePath || "/data/guanghulab/secrets-vault/auth-requests.json"); // ─── Gitea OAuth2 配置 ──────────────────────────────────── const oauthConfig = giteaOAuth || { clientId: process.env.GITEA_OAUTH_CLIENT_ID || "", clientSecret: process.env.GITEA_OAUTH_CLIENT_SECRET || "", authorizeUrl: (process.env.GITEA_URL || "https://guanghulab.com/git") + "/login/oauth/authorize", tokenUrl: (process.env.GITEA_URL || "https://guanghulab.com/git") + "/login/oauth/access_token", redirectUri: process.env.GITEA_OAUTH_REDIRECT_URI || "https://guanghulab.com/admin/auth/gitea/callback", scope: "repository", }; // ─── 列出待授权请求 ──────────────────────────────────────── router.get("/pending", (_req, res) => { const pending = store.listPending(); res.json({ _sovereign: "TCS-0002∞ · 国作登字-2026-A-00037559", count: pending.length, requests: pending.map(r => ({ id: r.id, persona: r.persona, purpose: r.purpose, keyName: r.keyName, requestedAt: r.requestedAt, description: r.description, })) }); }); // ─── 人格体发起授权请求 ──────────────────────────────────── router.post("/request", express.json({ limit: "16kb" }), (req, res) => { const { persona, purpose, keyName, description, tokenType } = req.body || {}; if (!persona || !purpose || !keyName) { return res.status(400).json({ error: true, code: "missing_fields", message: "缺少必填项: persona(人格体名)、purpose(用途)、keyName(密钥名)" }); } // 如果密钥已存在,直接告诉人格体 const existing = vault.get(keyName); if (existing) { return res.json({ status: "already_configured", keyName, message: `密钥 ${keyName} 已配置,可直接使用`, masked: maskValueSafe(existing), }); } const authRequest = store.add({ id: `auth-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`, persona: persona || "铸渊", purpose, keyName, description: description || `${persona} 需要 ${keyName} 用于${purpose}`, tokenType: tokenType || "random", status: "pending", requestedAt: new Date().toISOString(), }); res.json({ status: "pending", requestId: authRequest.id, message: `授权请求已创建,等待用户审批`, approvalUrl: `/admin/auth/approve.html?id=${authRequest.id}`, }); }); // ─── 用户批准授权 ────────────────────────────────────────── router.post("/:id/approve", express.json({ limit: "16kb" }), (req, res) => { const authReq = store.get(req.params.id); if (!authReq) { return res.status(404).json({ error: true, code: "not_found", message: "授权请求不存在" }); } if (authReq.status !== "pending") { return res.status(400).json({ error: true, code: "already_processed", message: `此请求已${authReq.status === "approved" ? "批准" : "拒绝"}` }); } // 生成 Token let tokenValue; if (authReq.tokenType === "gitea") { tokenValue = generateGiteaToken(); } else if (req.body && req.body.customValue) { // 用户自己设定的值 tokenValue = String(req.body.customValue); } else { tokenValue = generateToken(authReq.keyName.split("_")[0].toLowerCase()); } // 存入 vault try { vault.set(authReq.keyName, tokenValue); } catch (e) { return res.status(500).json({ error: true, code: "vault_write_failed", message: "密钥保存失败: " + e.message }); } // 更新请求状态 store.update(authReq.id, { status: "approved", approvedAt: new Date().toISOString(), approvedBy: req.body?.approvedBy || "user", }); res.json({ status: "approved", keyName: authReq.keyName, masked: maskValueSafe(tokenValue), length: tokenValue.length, message: `✅ 已授权!密钥 ${authReq.keyName} 已自动保存。你可以在密钥管理页查看和管理。`, }); }); // ─── 用户拒绝授权 ────────────────────────────────────────── router.post("/:id/reject", (_req, res) => { const authReq = store.get(_req.params.id); if (!authReq) { return res.status(404).json({ error: true, code: "not_found", message: "授权请求不存在" }); } store.update(authReq.id, { status: "rejected", rejectedAt: new Date().toISOString(), }); res.json({ status: "rejected", message: `已拒绝 ${authReq.persona} 对 ${authReq.keyName} 的授权请求。`, }); }); // ─── Gitea OAuth2 授权入口 ───────────────────────────────── router.get("/gitea/authorize", (_req, res) => { if (!oauthConfig.clientId) { return res.status(500).json({ error: true, code: "oauth_not_configured", message: "Gitea OAuth2 未配置" }); } const state = crypto.randomBytes(16).toString("hex"); // 存储 state 用于回调验证 store.add({ id: `oauth-${state}`, persona: "铸渊", purpose: "Gitea 仓库访问", keyName: "GITEA_TOKEN", description: "通过 Gitea OAuth2 授权,让铸渊可以访问和管理仓库", tokenType: "gitea", status: "oauth_pending", requestedAt: new Date().toISOString(), oauthState: state, }); const authorizeUrl = `${oauthConfig.authorizeUrl}?client_id=${oauthConfig.clientId}&redirect_uri=${encodeURIComponent(oauthConfig.redirectUri)}&response_type=code&state=${state}&scope=${oauthConfig.scope}`; res.redirect(authorizeUrl); }); // ─── Gitea OAuth2 回调 ───────────────────────────────────── router.get("/gitea/callback", async (req, res) => { const { code, state } = req.query || {}; if (!code || !state) { return res.status(400).send("授权失败:缺少 code 或 state"); } // 找到对应的 OAuth 请求 const oauthReq = store.list().find(r => r.oauthState === state); if (!oauthReq) { return res.status(400).send("授权失败:state 不匹配"); } // 用 code 换 token try { const tokenResponse = await exchangeCodeForToken(code, oauthConfig); const tokenValue = tokenResponse.access_token; if (!tokenValue) { throw new Error("Gitea 未返回 access_token"); } // 存入 vault vault.set("GITEA_TOKEN", tokenValue); store.update(oauthReq.id, { status: "approved", approvedAt: new Date().toISOString(), approvedBy: "gitea_oauth", }); res.send(` 授权成功

✅ 授权成功!

铸渊已获得 Gitea 仓库访问权限。

密钥已自动保存,你可以在密钥管理页查看和管理。

守护: 铸渊 · ICE-GL-ZY001 · TCS-0002∞

`); } catch (e) { res.status(500).send(`授权失败:${e.message}`); } }); return router; } // ─── OAuth2 Token 交换 ──────────────────────────────────────── async function exchangeCodeForToken(code, config) { const https = require("https"); const http = require("http"); const postData = new URLSearchParams({ client_id: config.clientId, client_secret: config.clientSecret, code, grant_type: "authorization_code", redirect_uri: config.redirectUri, }).toString(); return new Promise((resolve, reject) => { const urlObj = new URL(config.tokenUrl); const lib = urlObj.protocol === "https:" ? https : http; const req = lib.request(urlObj, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", }, }, (res) => { let body = ""; res.on("data", (d) => body += d); res.on("end", () => { try { resolve(JSON.parse(body)); } catch (e) { reject(new Error("Token 响应解析失败: " + body.slice(0, 200))); } }); }); req.on("error", reject); req.write(postData); req.end(); }); } // ─── 安全遮罩 ───────────────────────────────────────────────── function maskValueSafe(v) { if (v == null) return null; const s = String(v); if (s.length === 0) return ""; if (s.length <= 4) return "••••"; if (s.length <= 12) return s.slice(0, 2) + "••••" + s.slice(-2); return s.slice(0, 4) + "••••••" + s.slice(-4); } module.exports = { buildAuthRouter };