173 lines
8.0 KiB
JavaScript
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 };
|