Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

173 lines
8.0 KiB
JavaScript

/*
* ════════════════════════════════════════════════════════════════
* 光湖 · 自部署密钥管理页 (Secrets Vault) · Express 后端入口
* Sovereign: TCS-0002∞ · ICE-GL∞ · 国作登字-2026-A-00037559
* 守护: 铸渊 · ICE-GL-ZY001
* 模板版本: 0.1.0
* ════════════════════════════════════════════════════════════════
*
* 服务器: GH-CVM-DOMAIN-PROD-01 / ZY-SVR-CN01 / 广州 2C2G
* 角色: guanghulab.com 域内 /admin/secrets 子站
*
* 设计理念:
* cc-003 · AutoDL host:port 不入 GitHub Secrets, 改入本地 vault
* 1 步保存立即生效 (vs Github Secrets 5 步 round-trip).
*
* cc-004 · 全中文 UI · 字段名走"用途"不走 secret 名
* Awen 在浏览器里能直接读懂, 不用记 ZY_AUTODL_HOST 是干啥的.
*
* 2C2G 现实:
* Express 单进程, pm2 max_memory_restart=128M
* 内存预算: portal=512M + vault=128M + nginx=~64M + buffer = 不到 1G,
* 留一半给系统. 跟 baton-005 A 节一致.
*
* 公网隔离:
* 本服务只监听 127.0.0.1:VAULT_PORT (默认 8080)
* nginx /admin/ location 只允许 127.0.0.1 + basic-auth
* 冰朔走 ssh -L 8080:127.0.0.1:8080 + 浏览器 localhost 访问
* **永不暴露公网**
*
* 主密钥管理:
* 首次启动随机生成 → /data/guanghulab/secrets-vault/.master chmod 600
* vault.enc 也 chmod 600
* **不上 git, 不上 COS, 不传冰朔** (cc-004 系统自管, 不让冰朔记)
*
* 路由:
* GET /admin/secrets 按工作流分组列出 + 遮罩值
* GET /admin/secrets/manifest 前端用的元数据 (用途/如何配置)
* POST /admin/secrets/:key 写入 {value}
* DELETE /admin/secrets/:key 删除
* POST /admin/secrets/autodl/save_and_refresh
* AutoDL 一键: vault 落 + 探活 + 写
* inference-endpoint.json + pm2 reload portal
*
* 内部 (本机) 路由 (神笔马良用):
* GET /internal/fetch/:key 127.0.0.1 only · 拉明文
*
* 静态:
* /admin/static/* → frontend/secrets-vault/assets
* /admin/ → frontend/secrets-vault/index.html
*
* 健康检查:
* GET /admin/__healthz {ok, vault_loaded, has_endpoint_path}
* ════════════════════════════════════════════════════════════════
*/
"use strict";
const path = require("path");
const fs = require("fs");
const express = require("express");
const { Vault } = require("./lib/vault");
const { buildRouter } = require("./routes/secrets");
const { buildInternalRouter } = require("./routes/internal");
const { createRateLimit } = require("./lib/rate-limit");
// ─── 配置 ─────────────────────────────────────────────────────
const PORT = parseInt(process.env.PORT || "8080", 10);
const HOST = process.env.HOST || "127.0.0.1"; // 严格本机
const VAULT_DIR = process.env.VAULT_DIR || "/data/guanghulab/secrets-vault";
const PORTAL_DATA_DIR = process.env.PORTAL_DATA_DIR || "/data/guanghulab/portal/data";
const ENDPOINT_PATH =
process.env.INFERENCE_ENDPOINT_PATH || path.join(PORTAL_DATA_DIR, "inference-endpoint.json");
const REPO_ROOT = path.resolve(__dirname, "..", "..");
const STATIC_DIR =
process.env.VAULT_STATIC_DIR || path.join(REPO_ROOT, "frontend", "secrets-vault");
const MANIFEST_PATH =
process.env.VAULT_MANIFEST_PATH ||
path.join(REPO_ROOT, "scripts", "preflight", "secrets-manifest.json");
// ─── Vault 启动 ────────────────────────────────────────────────
const vault = new Vault(VAULT_DIR);
const masterInfo = vault.loadOrCreateMaster();
// ─── Express ──────────────────────────────────────────────────
const app = express();
app.disable("x-powered-by");
// 不开 trust proxy: req.ip 直接拿 socket 地址, 给 internal 路由判本机用
// nginx 只是 hop, 我们这里只接受 127.0.0.1, 不需要 X-Forwarded-For
// ─── 健康检查 (basic-auth 之外, 给 nginx /__healthz 探活) ─────
app.get("/admin/__healthz", (_req, res) => {
res.json({
ok: true,
server: "GH-CVM-DOMAIN-PROD-01",
role: "secrets-vault",
sovereign: "TCS-0002∞ · 国作登字-2026-A-00037559",
vault_dir: VAULT_DIR,
has_master: fs.existsSync(vault.masterPath),
has_vault_enc: fs.existsSync(vault.encPath),
endpoint_path: ENDPOINT_PATH,
ts: new Date().toISOString()
});
});
// ─── 限流 (纵深防御 · nginx 在前层也限) ────────────────────
// admin: 静态/路由用宽松限流; internal: 神笔马良调, 给紧一点
const adminLimit = createRateLimit({ windowMs: 60_000, max: 240 });
const internalLimit = createRateLimit({ windowMs: 60_000, max: 600 });
// ─── Admin 路由 (basic-auth 由 nginx 在前面拦, app 这层不再做) ─
app.use(
"/admin/secrets",
adminLimit,
buildRouter({ vault, manifestPath: MANIFEST_PATH, endpointPath: ENDPOINT_PATH })
);
// ─── 静态 (前端) ───────────────────────────────────────────────
app.use(
"/admin/static",
adminLimit,
express.static(path.join(STATIC_DIR, "assets"), { fallthrough: true })
);
app.get(["/admin", "/admin/"], adminLimit, (_req, res, next) => {
const idx = path.join(STATIC_DIR, "index.html");
fs.access(idx, fs.constants.R_OK, (err) => {
if (err) return next();
res.sendFile(idx);
});
});
// ─── 内部 (本机) 路由 — 神笔马良 ────────────────────────────────
app.use("/internal", internalLimit, buildInternalRouter({ vault }));
// ─── 404 + 错误 ───────────────────────────────────────────────
app.use((req, res) => {
if (req.path.startsWith("/admin/") || req.path.startsWith("/internal/")) {
res
.status(404)
.json({ error: true, code: "not_found", message: "接口不存在: " + req.path });
} else {
res.status(404).type("text/plain").send("404");
}
});
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, _next) => {
const msg = (err && err.message) || "未知错误";
// eslint-disable-next-line no-console
console.error("[vault] 内部错误:", err);
if (res.headersSent) return;
res
.status(err && err.status ? err.status : 500)
.json({ error: true, code: "internal_error", message: "内部错误: " + msg });
});
// ─── 启动 ─────────────────────────────────────────────────────
if (require.main === module) {
app.listen(PORT, HOST, () => {
// eslint-disable-next-line no-console
console.log(
`[vault] 已启动 · ${HOST}:${PORT} · vault_dir=${VAULT_DIR} · master_${masterInfo.created ? "created" : "loaded"}`
);
// eslint-disable-next-line no-console
console.log(`[vault] 静态目录: ${STATIC_DIR}`);
// eslint-disable-next-line no-console
console.log(`[vault] 推理端点写入文件: ${ENDPOINT_PATH}`);
// eslint-disable-next-line no-console
console.log("[vault] 守护: 铸渊 · ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559");
});
}
module.exports = { app, vault };