Bump secrets-mcp to 0.2.2 and sync Cargo.lock. Add home.html landing with SEO and footer link to the refining/secrets repository; serve it at / and expose /login for sign-in. Update OAuth error redirects and dashboard unauthenticated redirects to /login. Improve login page meta tags, back-home link, and OAuth error alert. Refresh llms.txt and robots.txt. Made-with: Cursor
270 lines
13 KiB
HTML
270 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="description" content="Secrets MCP:基于 Model Context Protocol 的密钥与配置管理。密码短语在浏览器本地 PBKDF2 派生,密文 AES-GCM 存储,完整审计与历史版本。">
|
||
<meta name="keywords" content="secrets management,MCP,Model Context Protocol,end-to-end encryption,AES-GCM,PBKDF2,API key,密钥管理">
|
||
<meta name="robots" content="index, follow">
|
||
<link rel="canonical" href="{{ base_url }}/">
|
||
<link rel="icon" href="/favicon.svg?v={{ version }}" type="image/svg+xml">
|
||
<title>Secrets MCP — 端到端加密的密钥管理</title>
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="{{ base_url }}/">
|
||
<meta property="og:title" content="Secrets MCP — 端到端加密的密钥管理">
|
||
<meta property="og:description" content="密码短语客户端派生,密文存储;MCP API 与 Web 控制台,多租户与审计。">
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="Secrets MCP — 端到端加密的密钥管理">
|
||
<meta name="twitter:description" content="密码短语客户端派生,密文存储;MCP API 与 Web 控制台,多租户与审计。">
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;600&family=Inter:wght@400;500;600&display=swap');
|
||
:root {
|
||
--bg: #0d1117;
|
||
--surface: #161b22;
|
||
--surface2: #21262d;
|
||
--border: #30363d;
|
||
--text: #e6edf3;
|
||
--text-muted: #8b949e;
|
||
--accent: #58a6ff;
|
||
--accent-hover: #79b8ff;
|
||
}
|
||
html, body { height: 100%; overflow: hidden; }
|
||
@supports (height: 100dvh) {
|
||
html, body { height: 100dvh; }
|
||
}
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: 'Inter', sans-serif;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.nav {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--surface);
|
||
}
|
||
.brand {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
}
|
||
.brand span { color: var(--accent); }
|
||
.nav-right { display: flex; align-items: center; gap: 14px; }
|
||
.lang-bar { display: flex; gap: 2px; background: rgba(255,255,255,0.04); border-radius: 6px; padding: 2px; }
|
||
.lang-btn {
|
||
padding: 4px 10px; border: none; background: none; color: var(--text-muted);
|
||
font-size: 12px; cursor: pointer; border-radius: 4px;
|
||
}
|
||
.lang-btn.active { background: var(--border); color: var(--text); }
|
||
.cta {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
padding: 8px 18px; border-radius: 8px; font-size: 13px; font-weight: 600;
|
||
text-decoration: none; border: 1px solid var(--accent);
|
||
background: rgba(88, 166, 255, 0.12); color: var(--accent);
|
||
transition: background 0.15s, color 0.15s;
|
||
}
|
||
.cta:hover { background: var(--accent); color: var(--bg); }
|
||
.main {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16px 24px 12px;
|
||
gap: 20px;
|
||
}
|
||
.hero { text-align: center; max-width: 720px; }
|
||
.hero h1 { font-size: clamp(20px, 4vw, 28px); font-weight: 600; margin-bottom: 8px; line-height: 1.25; }
|
||
.hero .tagline { color: var(--text-muted); font-size: clamp(13px, 2vw, 15px); line-height: 1.5; }
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
width: 100%;
|
||
max-width: 900px;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.grid { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.grid { grid-template-columns: 1fr; gap: 8px; }
|
||
.main { justify-content: flex-start; padding-top: 12px; }
|
||
}
|
||
.card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 14px 14px 12px;
|
||
min-height: 0;
|
||
}
|
||
.card-icon {
|
||
width: 32px; height: 32px; border-radius: 8px;
|
||
background: var(--surface2);
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 10px; color: var(--accent);
|
||
}
|
||
.card-icon svg { width: 18px; height: 18px; }
|
||
.card h2 { font-size: 13px; font-weight: 600; margin-bottom: 6px; line-height: 1.3; }
|
||
.card p { font-size: 12px; color: var(--text-muted); line-height: 1.45; }
|
||
.foot {
|
||
flex-shrink: 0;
|
||
text-align: center;
|
||
padding: 8px 16px 12px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
border-top: 1px solid var(--border);
|
||
background: var(--surface);
|
||
}
|
||
.foot a { color: var(--accent); text-decoration: none; }
|
||
.foot a:hover { text-decoration: underline; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="nav">
|
||
<a class="brand" href="/">secrets<span>-mcp</span></a>
|
||
<div class="nav-right">
|
||
<div class="lang-bar">
|
||
<button type="button" class="lang-btn" onclick="setLang('zh-CN')">简</button>
|
||
<button type="button" class="lang-btn" onclick="setLang('zh-TW')">繁</button>
|
||
<button type="button" class="lang-btn" onclick="setLang('en')">EN</button>
|
||
</div>
|
||
{% if is_logged_in %}
|
||
<a class="cta" href="/dashboard" data-i18n="ctaDashboard">进入控制台</a>
|
||
{% else %}
|
||
<a class="cta" href="/login" data-i18n="ctaLogin">登录</a>
|
||
{% endif %}
|
||
</div>
|
||
</header>
|
||
<main class="main">
|
||
<div class="hero">
|
||
<h1 data-i18n="heroTitle">端到端加密的密钥与配置管理</h1>
|
||
<p class="tagline" data-i18n="heroTagline">Streamable HTTP MCP 与 Web 控制台:元数据与密文分库存储,密钥永不离开你的客户端逻辑。</p>
|
||
</div>
|
||
<div class="grid">
|
||
<article class="card">
|
||
<div class="card-icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 11c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v3c0 1.66 1.34 3 3 3z"/><path d="M19 10v1a7 7 0 01-14 0v-1"/><path d="M12 14v7M9 18h6"/></svg>
|
||
</div>
|
||
<h2 data-i18n="c1t">客户端密钥派生</h2>
|
||
<p data-i18n="c1d">PBKDF2-SHA256(约 60 万次)在浏览器本地从密码短语派生密钥;服务端仅保存盐与校验值,不持有密码或明文主密钥。</p>
|
||
</article>
|
||
<article class="card">
|
||
<div class="card-icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
|
||
</div>
|
||
<h2 data-i18n="c2t">AES-256-GCM 加密</h2>
|
||
<p data-i18n="c2d">敏感字段以 AES-GCM 密文落库;Web 端在本地加解密,明文默认不经过服务端持久化。</p>
|
||
</article>
|
||
<article class="card">
|
||
<div class="card-icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg>
|
||
</div>
|
||
<h2 data-i18n="c3t">审计与历史</h2>
|
||
<p data-i18n="c3d">操作写入审计日志;条目与密文保留历史版本,支持按版本查看与恢复。</p>
|
||
</article>
|
||
</div>
|
||
</main>
|
||
<footer class="foot">
|
||
<span data-i18n="versionLabel">版本</span> {{ version }} ·
|
||
<a href="/llms.txt">llms.txt</a>
|
||
<span data-i18n="sep"> · </span>
|
||
<a href="https://gitea.refining.dev/refining/secrets" target="_blank" rel="noopener noreferrer" data-i18n="footRepo">源码仓库</a>
|
||
{% if !is_logged_in %}
|
||
<span data-i18n="sep"> · </span>
|
||
<a href="/login" data-i18n="footLogin">登录</a>
|
||
{% endif %}
|
||
</footer>
|
||
<script>
|
||
const T = {
|
||
'zh-CN': {
|
||
docTitle: 'Secrets MCP — 端到端加密的密钥管理',
|
||
ctaDashboard: '进入控制台',
|
||
ctaLogin: '登录',
|
||
heroTitle: '端到端加密的密钥与配置管理',
|
||
heroTagline: 'Streamable HTTP MCP 与 Web 控制台:元数据与密文分库存储,密钥永不离开你的客户端逻辑。',
|
||
c1t: '客户端密钥派生',
|
||
c1d: 'PBKDF2-SHA256(约 60 万次)在浏览器本地从密码短语派生密钥;服务端仅保存盐与校验值,不持有密码或明文主密钥。',
|
||
c2t: 'AES-256-GCM 加密',
|
||
c2d: '敏感字段以 AES-GCM 密文落库;Web 端在本地加解密,明文默认不经过服务端持久化。',
|
||
c3t: '审计与历史',
|
||
c3d: '操作写入审计日志;条目与密文保留历史版本,支持按版本查看与恢复。',
|
||
versionLabel: '版本',
|
||
sep: ' · ',
|
||
footRepo: '源码仓库',
|
||
footLogin: '登录',
|
||
},
|
||
'zh-TW': {
|
||
docTitle: 'Secrets MCP — 端到端加密的金鑰管理',
|
||
ctaDashboard: '進入控制台',
|
||
ctaLogin: '登入',
|
||
heroTitle: '端到端加密的金鑰與設定管理',
|
||
heroTagline: 'Streamable HTTP MCP 與 Web 控制台:中繼資料與密文分庫儲存,金鑰不離開你的用戶端邏輯。',
|
||
c1t: '用戶端金鑰派生',
|
||
c1d: 'PBKDF2-SHA256(約 60 萬次)在瀏覽器本地從密碼片語派生金鑰;伺服端僅保存鹽與校驗值,不持有密碼或明文主金鑰。',
|
||
c2t: 'AES-256-GCM 加密',
|
||
c2d: '敏感欄位以 AES-GCM 密文落庫;Web 端在本地加解密,明文預設不經伺服端持久化。',
|
||
c3t: '稽核與歷史',
|
||
c3d: '操作寫入稽核日誌;條目與密文保留歷史版本,支援依版本檢視與還原。',
|
||
versionLabel: '版本',
|
||
sep: ' · ',
|
||
footRepo: '原始碼倉庫',
|
||
footLogin: '登入',
|
||
},
|
||
'en': {
|
||
docTitle: 'Secrets MCP — End-to-end encrypted secrets',
|
||
ctaDashboard: 'Open dashboard',
|
||
ctaLogin: 'Sign in',
|
||
heroTitle: 'End-to-end encrypted secrets and configuration',
|
||
heroTagline: 'Streamable HTTP MCP plus web console: metadata and ciphertext stored separately; keys stay on your client.',
|
||
c1t: 'Client-side key derivation',
|
||
c1d: 'PBKDF2-SHA256 (~600k iterations) derives keys from your passphrase in the browser; the server stores only salt and a verification blob, never your password or raw master key.',
|
||
c2t: 'AES-256-GCM',
|
||
c2d: 'Secret fields are stored as AES-GCM ciphertext; the web UI encrypts and decrypts locally so plaintext is not persisted server-side by default.',
|
||
c3t: 'Audit and history',
|
||
c3d: 'Operations are audited; entries and secrets keep version history for review and rollback.',
|
||
versionLabel: 'Version',
|
||
sep: ' · ',
|
||
footRepo: 'Source repository',
|
||
footLogin: 'Sign in',
|
||
}
|
||
};
|
||
|
||
let currentLang = localStorage.getItem('lang') || 'zh-CN';
|
||
|
||
function t(key) {
|
||
return (T[currentLang] && T[currentLang][key]) || T['en'][key] || key;
|
||
}
|
||
|
||
function applyLang() {
|
||
document.documentElement.lang = currentLang;
|
||
document.title = t('docTitle');
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const key = el.getAttribute('data-i18n');
|
||
el.textContent = t(key);
|
||
});
|
||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||
const map = { 'zh-CN': '简', 'zh-TW': '繁', 'en': 'EN' };
|
||
btn.classList.toggle('active', btn.textContent === map[currentLang]);
|
||
});
|
||
}
|
||
|
||
function setLang(lang) {
|
||
currentLang = lang;
|
||
localStorage.setItem('lang', lang);
|
||
applyLang();
|
||
}
|
||
|
||
applyLang();
|
||
</script>
|
||
</body>
|
||
</html>
|