Rebrand: dynamic POPs from DB, NOC dashboard, looking glass, terminology overhaul
- POPs map on /hosted now generated from clavitor.db (28 POPs, 8 live) - Added /glass (looking glass with client-side latency) - Added /noc?pin=250365 (NOC dashboard with telemetry) - Added POST /telemetry endpoint for POP agent heartbeats - Encryption terminology: Vault/Credential/Identity (no more sealed/agent/L1-L3) - License: MIT → Elastic License 2.0 - Capitalize Clavitor in all prose - MCP references → CLI (agents use CLI, not MCP) - GitHub links disabled (project not public yet) - New favicon (black square logo) + apple-touch-icon - Darker worldmap land/borders - Added CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1606d3fdd4
commit
22fed471f4
|
|
@ -0,0 +1,59 @@
|
|||
# Clavitor Website — clavitor.com
|
||||
|
||||
## Architecture
|
||||
- Go web server (`main.go`) with `go:embed` for templates, CSS, SVGs, PNGs
|
||||
- Templates in `templates/*.tmpl`, single CSS in `clavitor.css`
|
||||
- SQLite DB: `clavitor.db` (pops, telemetry, uptime, incidents, accounts, vaults, sessions)
|
||||
- Dev mode: auto-detected when `templates/` dir exists on disk — reloads templates per request, but CSS/SVGs require rebuild (`go:embed`)
|
||||
- Port 8099
|
||||
|
||||
## Build & Run
|
||||
```
|
||||
CGO_ENABLED=1 go build -o clavitor-web .
|
||||
./clavitor-web
|
||||
```
|
||||
CSS and SVG changes require rebuild (embedded at compile time). Template changes reload in dev mode.
|
||||
|
||||
## Brand & Design
|
||||
- Light mode only. Single source of truth: `clavitor.css`
|
||||
- Logo: the black square (`#0A0A0A`). favicon.svg = black square
|
||||
- Colors: black `#0A0A0A` (brand), red `#DC2626` (accent), light red `#F5B7B7` (planned/secondary), grayscale
|
||||
- No purple. No green (except inherited SVG diagrams). Red is the only accent.
|
||||
- Square shapes for permanent UI elements. Circles only for transient animations (pulses, "You" dot)
|
||||
- Fonts: Figtree (body), JetBrains Mono (code/monospace)
|
||||
- No inline styles, no CSS in templates. Everything in clavitor.css.
|
||||
|
||||
## Encryption Terminology
|
||||
- **Vault Encryption** — whole vault at rest
|
||||
- **Credential Encryption** — per-field, server-side (AI agents can read via MCP)
|
||||
- **Identity Encryption** — per-field, client-side via WebAuthn PRF (Touch ID only, server cannot decrypt)
|
||||
- Never use "sealed fields", "agent fields", "L1", "L2", "L3"
|
||||
|
||||
## POPs (Points of Presence)
|
||||
- Stored in `pops` table in clavitor.db — the single source of truth
|
||||
- Map on /hosted is generated dynamically from DB via JavaScript
|
||||
- Zürich = HQ, black dot, larger (11×11). Live POPs = red. Planned = light red.
|
||||
- "You" visitor dot = circle (not square — "you" is not clavitor)
|
||||
|
||||
## Key URLs
|
||||
- `/hosted` — hosted product page with dynamic world map
|
||||
- `/glass` — looking glass (latency from user's browser)
|
||||
- `/noc?pin=250365` — NOC dashboard (telemetry, read-only, hardcoded PIN)
|
||||
- `/telemetry` — POST endpoint for POP agent heartbeats (no auth)
|
||||
- `/ping` — server-side TCP ping (for diagnostics)
|
||||
|
||||
## Vault Binary
|
||||
- Source: `~/dev/clavitor/clovis/clovis-vault/`
|
||||
- Build for ARM64: `cd ~/dev/clavitor/clovis/clovis-vault && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o clavitor-linux-arm64 ./cmd/clavitor`
|
||||
- All POPs are ARM64 (AWS t4g.micro)
|
||||
- Vault runs on port 1984 with TLS
|
||||
- Has `/ping` endpoint (11 bytes, no DB, CORS via middleware) for looking glass
|
||||
- Has `/health` endpoint (heavier, queries DB)
|
||||
|
||||
## Providers
|
||||
- AWS: most POPs (free tier t4g.micro)
|
||||
- LightNode: Santiago, Bogotá, Manila, Dhaka
|
||||
- ishosting: Istanbul, Almaty
|
||||
- HostAfrica: Lagos, Nairobi
|
||||
- Voyager NZ → switched to AWS for Auckland
|
||||
- Rackmill: Perth
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 408 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="80" height="80" role="img" aria-label="Clavitor">
|
||||
<title>Clavitor</title>
|
||||
<rect x="5" y="5" width="90" height="90" fill="#0A0A0A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 212 B |
Binary file not shown.
|
|
@ -1,5 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" rx="20" fill="#0A1628"/>
|
||||
<text y=".9em" font-size="72" x="14" fill="#22C55E" font-family="monospace" font-weight="bold">v</text>
|
||||
<text y=".9em" font-size="72" x="44" fill="#22C55E" font-family="monospace" font-weight="bold" opacity="0.7">84</text>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="80" height="80" role="img" aria-label="Clavitor">
|
||||
<title>Clavitor</title>
|
||||
<rect x="5" y="5" width="90" height="90" fill="#0A0A0A"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 212 B |
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
|
@ -20,7 +21,7 @@ import (
|
|||
//go:embed templates/*.tmpl
|
||||
var tmplFS embed.FS
|
||||
|
||||
//go:embed *.svg *.css
|
||||
//go:embed *.svg *.css *.png
|
||||
var static embed.FS
|
||||
|
||||
var templates *template.Template
|
||||
|
|
@ -115,7 +116,7 @@ func main() {
|
|||
loadTemplates()
|
||||
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", "clavitor.db?mode=ro")
|
||||
db, err = sql.Open("sqlite3", "clavitor.db")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open clavitor.db: %v", err)
|
||||
}
|
||||
|
|
@ -170,6 +171,136 @@ func main() {
|
|||
http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) {
|
||||
render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"})
|
||||
})
|
||||
// NOC telemetry ingest — agents POST here
|
||||
http.HandleFunc("/telemetry", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
var t struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Version string `json:"version"`
|
||||
Hostname string `json:"hostname"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemTotalMB int64 `json:"memory_total_mb"`
|
||||
MemUsedMB int64 `json:"memory_used_mb"`
|
||||
DiskTotalMB int64 `json:"disk_total_mb"`
|
||||
DiskUsedMB int64 `json:"disk_used_mb"`
|
||||
Load1m float64 `json:"load_1m"`
|
||||
VaultCount int `json:"vault_count"`
|
||||
VaultSizeMB float64 `json:"vault_size_mb"`
|
||||
VaultEntries int `json:"vault_entries"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&t); err != nil || t.NodeID == "" {
|
||||
http.Error(w, `{"error":"bad payload"}`, 400)
|
||||
return
|
||||
}
|
||||
db.Exec(`INSERT INTO telemetry (node_id, version, hostname, uptime_seconds, cpu_percent, memory_total_mb, memory_used_mb, disk_total_mb, disk_used_mb, load_1m, vault_count, vault_size_mb, vault_entries, mode) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
t.NodeID, t.Version, t.Hostname, t.UptimeSeconds, t.CPUPercent, t.MemTotalMB, t.MemUsedMB, t.DiskTotalMB, t.DiskUsedMB, t.Load1m, t.VaultCount, t.VaultSizeMB, t.VaultEntries, t.Mode)
|
||||
// Update daily uptime
|
||||
today := time.Now().Format("2006-01-02")
|
||||
db.Exec(`INSERT OR REPLACE INTO uptime (node_id, date, status) VALUES (?, ?, 'operational')`, t.NodeID, today)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"ok":true}`))
|
||||
})
|
||||
|
||||
// NOC API — latest telemetry per node
|
||||
nocPin := func(r *http.Request) bool { return r.URL.Query().Get("pin") == "250365" }
|
||||
|
||||
http.HandleFunc("/noc/api/telemetry", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !nocPin(r) { http.NotFound(w, r); return }
|
||||
rows, err := db.Query(`SELECT t.node_id, t.received_at, t.version, t.hostname, t.uptime_seconds, t.cpu_percent, t.memory_total_mb, t.memory_used_mb, t.disk_total_mb, t.disk_used_mb, t.load_1m, t.vault_count, t.vault_size_mb, t.vault_entries, t.mode FROM telemetry t INNER JOIN (SELECT node_id, MAX(id) as max_id FROM telemetry GROUP BY node_id) latest ON t.id = latest.max_id`)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"query failed"}`, 500)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
type Tel struct {
|
||||
NodeID string `json:"node_id"`
|
||||
ReceivedAt int64 `json:"received_at"`
|
||||
Version string `json:"version"`
|
||||
Hostname string `json:"hostname"`
|
||||
UptimeSec int64 `json:"uptime_seconds"`
|
||||
CPU float64 `json:"cpu_percent"`
|
||||
MemTotal int64 `json:"memory_total_mb"`
|
||||
MemUsed int64 `json:"memory_used_mb"`
|
||||
DiskTotal int64 `json:"disk_total_mb"`
|
||||
DiskUsed int64 `json:"disk_used_mb"`
|
||||
Load1m float64 `json:"load_1m"`
|
||||
VaultCount int `json:"vault_count"`
|
||||
VaultSizeMB float64 `json:"vault_size_mb"`
|
||||
VaultEntries int `json:"vault_entries"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
var list []Tel
|
||||
for rows.Next() {
|
||||
var t Tel
|
||||
rows.Scan(&t.NodeID, &t.ReceivedAt, &t.Version, &t.Hostname, &t.UptimeSec, &t.CPU, &t.MemTotal, &t.MemUsed, &t.DiskTotal, &t.DiskUsed, &t.Load1m, &t.VaultCount, &t.VaultSizeMB, &t.VaultEntries, &t.Mode)
|
||||
list = append(list, t)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{"telemetry": list})
|
||||
})
|
||||
|
||||
http.HandleFunc("/noc/api/nodes", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !nocPin(r) { http.NotFound(w, r); return }
|
||||
pops := loadPops()
|
||||
type N struct {
|
||||
ID string `json:"ID"`
|
||||
Status string `json:"Status"`
|
||||
}
|
||||
var nodes []N
|
||||
for _, p := range pops {
|
||||
nodes = append(nodes, N{ID: p.City, Status: p.Status})
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{"nodes": nodes})
|
||||
})
|
||||
|
||||
http.HandleFunc("/noc/api/telemetry/history", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !nocPin(r) { http.NotFound(w, r); return }
|
||||
node := r.URL.Query().Get("node")
|
||||
limit := r.URL.Query().Get("limit")
|
||||
if limit == "" { limit = "60" }
|
||||
rows, err := db.Query(`SELECT received_at, cpu_percent, memory_used_mb, memory_total_mb FROM telemetry WHERE node_id = ? ORDER BY id DESC LIMIT ?`, node, limit)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"query failed"}`, 500)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
type H struct {
|
||||
TS int64 `json:"ts"`
|
||||
CPU float64 `json:"cpu"`
|
||||
MemUsed int64 `json:"mem_used_mb"`
|
||||
MemTotal int64 `json:"mem_total_mb"`
|
||||
}
|
||||
var hist []H
|
||||
for rows.Next() {
|
||||
var h H
|
||||
rows.Scan(&h.TS, &h.CPU, &h.MemUsed, &h.MemTotal)
|
||||
hist = append(hist, h)
|
||||
}
|
||||
// Reverse so oldest first
|
||||
for i, j := 0, len(hist)-1; i < j; i, j = i+1, j-1 {
|
||||
hist[i], hist[j] = hist[j], hist[i]
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{"history": hist})
|
||||
})
|
||||
|
||||
// NOC dashboard — hardcoded PIN, read-only, not a security boundary
|
||||
http.HandleFunc("/noc", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("pin") != "250365" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data := PageData{Page: "noc", Title: "NOC — clavitor"}
|
||||
data.Pops = loadPops()
|
||||
render(w, data)
|
||||
})
|
||||
|
||||
http.HandleFunc("/glass", func(w http.ResponseWriter, r *http.Request) {
|
||||
data := PageData{Page: "glass", Title: "Looking Glass — clavitor"}
|
||||
data.Pops = loadPops()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<title>{{.Title}}</title>
|
||||
{{if .Desc}}<meta name="description" content="{{.Desc}}">{{end}}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400..700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
|
|
@ -18,7 +19,7 @@
|
|||
<div class="container nav-inner">
|
||||
<a href="/" class="nav-logo"><span class="logo-lockup logo-lockup-nav"><span class="logo-lockup-square"></span><span class="logo-lockup-text"><span class="logo-lockup-wordmark">CLAVITOR</span><span class="logo-lockup-tagline">Black-box credential issuance</span></span></span></a>
|
||||
<div class="nav-links">
|
||||
<a href="https://github.com/clavitor/clavitor" target="_blank" rel="noopener" class="nav-link">GitHub</a>
|
||||
<span class="nav-link" style="opacity:0.4;cursor:default">GitHub</span>
|
||||
<a href="/hosted" class="nav-link{{if eq .ActiveNav "hosted"}} active{{end}}">Hosted</a>
|
||||
<a href="/pricing" class="nav-link{{if eq .ActiveNav "pricing"}} active{{end}}">Pricing</a>
|
||||
<a href="/install" class="nav-link{{if eq .ActiveNav "install"}} active{{end}}">Self-host</a>
|
||||
|
|
@ -36,11 +37,13 @@
|
|||
{{else if eq .Page "sources"}}{{template "sources" .}}
|
||||
{{else if eq .Page "styleguide"}}{{template "styleguide" .}}
|
||||
{{else if eq .Page "glass"}}{{template "glass" .}}
|
||||
{{else if eq .Page "noc"}}{{template "noc" .}}
|
||||
{{end}}
|
||||
{{if ne .Page "styleguide"}}{{template "footer"}}{{end}}
|
||||
{{if eq .Page "index"}}{{template "index-script"}}
|
||||
{{else if eq .Page "hosted"}}{{template "hosted-script" .}}
|
||||
{{else if eq .Page "glass"}}{{template "glass-script"}}
|
||||
{{else if eq .Page "noc"}}{{template "noc-script"}}
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
<div class="footer-inner">
|
||||
<div class="footer-links">
|
||||
<a href="/" class="vaultname">clavitor</a>
|
||||
<a href="https://github.com/clavitor/clavitor" target="_blank" rel="noopener">GitHub</a>
|
||||
<span style="opacity:0.4">GitHub</span>
|
||||
<a href="#">Discord</a>
|
||||
<a href="#">X</a>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="/privacy">Privacy</a>
|
||||
<a href="/terms">Terms</a>
|
||||
<span>MIT License</span>
|
||||
<span>Elastic License 2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-copy">Built for humans with AI assistants.</p>
|
||||
|
|
|
|||
|
|
@ -114,12 +114,12 @@
|
|||
<div class="card card-hover">
|
||||
<div class="feature-icon red"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg></div>
|
||||
<h3 class="mb-3">Policy isn't protection</h3>
|
||||
<p>"We will not read your passwords" is a promise. Promises can be broken, compelled, or hacked. clavitor cannot read your Credential or Identity fields — not will not. <strong>Cannot.</strong> The key was never here.</p>
|
||||
<p>"We will not read your passwords" is a promise. Promises can be broken, compelled, or hacked. Clavitor cannot read your Credential or Identity fields — not will not. <strong>Cannot.</strong> The key was never here.</p>
|
||||
</div>
|
||||
<div class="card card-hover">
|
||||
<div class="feature-icon red"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg></div>
|
||||
<h3 class="mb-3">AI agents need credentials now</h3>
|
||||
<p>Your AI needs your GitHub token to deploy. It shouldn't also be able to see your passport. Every other manager is all-or-nothing. clavitor gives agents exactly what they need — nothing more.</p>
|
||||
<p>Your AI needs your GitHub token to deploy. It shouldn't also be able to see your passport. Every other manager is all-or-nothing. Clavitor gives agents exactly what they need — nothing more.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@
|
|||
<div class="card alt">
|
||||
<span class="badge accent mb-4">Credential Encryption</span>
|
||||
<h3 class="mb-3">AI-readable</h3>
|
||||
<p class="mb-4">Encrypted at rest, decryptable by the vault server. Your AI agent reads these via MCP.</p>
|
||||
<p class="mb-4">Encrypted at rest, decryptable by the vault server. Your AI agent accesses these via the CLI.</p>
|
||||
<ul class="checklist">
|
||||
<li>API keys & tokens</li>
|
||||
<li>SSH keys</li>
|
||||
|
|
@ -179,12 +179,12 @@
|
|||
<div class="card card-hover">
|
||||
<div class="feature-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div>
|
||||
<h3 class="mb-3">AI-powered 2FA</h3>
|
||||
<p>Store TOTP secrets as Credential fields. Your AI generates time-based codes on demand via MCP — no more switching to your phone.</p>
|
||||
<p>Store TOTP secrets as Credential fields. Your AI generates time-based codes on demand via the CLI — no more switching to your phone.</p>
|
||||
</div>
|
||||
<div class="card card-hover">
|
||||
<div class="feature-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg></div>
|
||||
<h3 class="mb-3">Scoped MCP tokens</h3>
|
||||
<p>Create separate MCP tokens per agent. Each token sees only its designated entries. Compromise one, the rest stay clean.</p>
|
||||
<h3 class="mb-3">Scoped agent tokens</h3>
|
||||
<p>Create separate tokens per agent. Each token sees only its designated entries. Compromise one, the rest stay clean.</p>
|
||||
</div>
|
||||
<div class="card card-hover">
|
||||
<div class="feature-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/></svg></div>
|
||||
|
|
@ -201,26 +201,22 @@
|
|||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- Multi-agent swarms -->
|
||||
<!-- Multi-agent -->
|
||||
<div class="section container">
|
||||
<div class="grid-2">
|
||||
<div>
|
||||
<h2 class="mb-4">10 agents.<br><span class="gradient-text">Each gets exactly what it needs.</span></h2>
|
||||
<p class="lead mb-6">Create scoped MCP tokens per agent. One compromised agent exposes one scope — not your entire vault.</p>
|
||||
<p class="lead mb-6">Create scoped CLI tokens per agent. One compromised agent exposes one scope — not your entire vault.</p>
|
||||
<p class="mb-4">Why not MCP? Because MCP gives the agent access to the vault — search, list, browse. That's too much. Clavitor's CLI gives the agent exactly the credentials it's scoped to. Nothing more. No browsing, no discovery, no surprise access.</p>
|
||||
<div class="code-block">
|
||||
<p class="code-label">~/.claude/mcp.json</p>
|
||||
<pre>{
|
||||
"mcpServers": {
|
||||
"vault-dev": {
|
||||
"url": "http://localhost:1984/mcp",
|
||||
"headers": { "Authorization": "Bearer <span class="prompt">mcp_dev_a3f8...</span>" }
|
||||
},
|
||||
"vault-social": {
|
||||
"url": "http://localhost:1984/mcp",
|
||||
"headers": { "Authorization": "Bearer <span class="prompt">mcp_social_7b2e...</span>" }
|
||||
}
|
||||
}
|
||||
}</pre>
|
||||
<p class="code-label">Agent workflow</p>
|
||||
<pre><span class="comment"># Agent fetches credential — encrypted, never plaintext</span>
|
||||
<span class="prompt">$</span> clavitor get github.token --agent dev --format env
|
||||
GITHUB_TOKEN=ghp_a3f8...
|
||||
|
||||
<span class="comment"># Scoped: dev agent can't see social credentials</span>
|
||||
<span class="prompt">$</span> clavitor get twitter.oauth --agent dev
|
||||
Error: access denied (scope: dev)</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -289,9 +285,9 @@
|
|||
<p class="lead mb-8">Four ways in. Each one designed for a different context. All pointing at the same encrypted store.</p>
|
||||
<div class="grid-2">
|
||||
<div class="card card-hover">
|
||||
<p class="label accent mb-3">MCP</p>
|
||||
<p class="label accent mb-3">CLI</p>
|
||||
<h3 class="mb-2">For AI agents</h3>
|
||||
<p>Claude, GPT, or any MCP-compatible agent can search credentials, fetch API keys, and generate 2FA codes — scoped to exactly what you allow.</p>
|
||||
<p>Agents call the CLI to fetch credentials — scoped per agent. Each agent sees only what it's been granted. No vault browsing, no discovery.</p>
|
||||
</div>
|
||||
<div class="card card-hover">
|
||||
<p class="label accent mb-3">Extension</p>
|
||||
|
|
@ -415,15 +411,14 @@
|
|||
<div class="comment"># Running on http://localhost:1984</div>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<p class="code-label">MCP config for Claude Code / Cursor / Codex</p>
|
||||
<pre>{
|
||||
"mcpServers": {
|
||||
"clavitor": {
|
||||
"url": "http://localhost:1984/mcp",
|
||||
"headers": { "Authorization": "Bearer <span class="prompt">mcp_your_token_here</span>" }
|
||||
}
|
||||
}
|
||||
}</pre>
|
||||
<p class="code-label">Agent access — scoped, encrypted</p>
|
||||
<pre><span class="comment"># Create a scoped token for your deploy agent</span>
|
||||
<span class="prompt">$</span> clavitor token create --scope deploy --name "CI pipeline"
|
||||
Token: <span class="highlight">ctk_deploy_9f2a...</span>
|
||||
|
||||
<span class="comment"># Agent fetches only what it's scoped to</span>
|
||||
<span class="prompt">$</span> clavitor get vercel.token --agent deploy
|
||||
VERCEL_TOKEN=tV3r...</pre>
|
||||
</div>
|
||||
<p class="mt-4"><a href="/install" class="btn btn-accent">Full install guide →</a></p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
{{define "install"}}
|
||||
<div class="hero container">
|
||||
<p class="label mb-3">Open source · MIT</p>
|
||||
<h1 class="mb-4">Self-host clavitor</h1>
|
||||
<p class="label mb-3">Open source · Elastic License 2.0</p>
|
||||
<h1 class="mb-4">Self-host Clavitor</h1>
|
||||
<p class="lead">One binary. No Docker. No Postgres. No Redis. Runs anywhere Go runs. You'll need a server with a public IP, DNS, and TLS if you want access from outside your network.</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -19,9 +19,9 @@
|
|||
<div class="code-block"><span class="prompt">$</span> curl -fsSL clavitor.com/install.sh | sh</div>
|
||||
<p class="mt-3 text-sm">Or download directly:</p>
|
||||
<div class="dl-links">
|
||||
<a href="https://github.com/clavitor/clavitor/releases/latest/download/clavitor-linux-amd64" class="btn btn-ghost btn-sm btn-mono">linux/amd64</a>
|
||||
<a href="https://github.com/clavitor/clavitor/releases/latest/download/clavitor-darwin-arm64" class="btn btn-ghost btn-sm btn-mono">darwin/arm64</a>
|
||||
<a href="https://github.com/clavitor/clavitor/releases/latest/download/clavitor-darwin-amd64" class="btn btn-ghost btn-sm btn-mono">darwin/amd64</a>
|
||||
<span class="btn btn-ghost btn-sm btn-mono" style="opacity:0.4;cursor:default">linux/amd64</span>
|
||||
<span class="btn btn-ghost btn-sm btn-mono" style="opacity:0.4;cursor:default">darwin/arm64</span>
|
||||
<span class="btn btn-ghost btn-sm btn-mono" style="opacity:0.4;cursor:default">darwin/amd64</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<p>A SQLite database is created automatically in <code>~/.clavitor/</code>.</p>
|
||||
<div class="code-block">
|
||||
<div><span class="prompt">$</span> clavitor</div>
|
||||
<div class="comment">clavitor running on http://localhost:1984</div>
|
||||
<div class="comment">Clavitor running on http://localhost:1984</div>
|
||||
<div class="comment">Database: ~/.clavitor/vault.db</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -56,20 +56,16 @@
|
|||
<div class="step">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-body">
|
||||
<h2>Configure MCP</h2>
|
||||
<p>Point your AI assistant at the vault. Works with Claude Code, Cursor, Codex, or any MCP-compatible client.</p>
|
||||
<p class="label mb-3">~/.claude/mcp.json</p>
|
||||
<div class="code-block"><pre>{
|
||||
"mcpServers": {
|
||||
"clavitor": {
|
||||
"url": "http://localhost:1984/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <span class="highlight">YOUR_MCP_TOKEN</span>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}</pre></div>
|
||||
<p class="mt-3 text-sm">Generate an MCP token from the web UI at <code>http://localhost:1984</code> after first run.</p>
|
||||
<h2>Configure agent access</h2>
|
||||
<p>Create a scoped token for each AI agent. Agents use the CLI to fetch credentials — encrypted in transit, never exposed in plaintext.</p>
|
||||
<div class="code-block">
|
||||
<div><span class="comment"># Create a scoped agent token</span></div>
|
||||
<div><span class="prompt">$</span> clavitor token create --scope dev --name "Claude Code"</div>
|
||||
<div class="comment">Token: <span class="highlight">ctk_dev_a3f8...</span></div>
|
||||
<div class="mt-2"><span class="comment"># Agent fetches credentials via CLI</span></div>
|
||||
<div><span class="prompt">$</span> clavitor get github.token --agent dev</div>
|
||||
</div>
|
||||
<p class="mt-3 text-sm">Manage tokens from the web UI at <code>http://localhost:1984</code> after first run.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -90,7 +86,7 @@
|
|||
<hr class="divider mb-8 mt-4">
|
||||
|
||||
<h2 class="mb-4">Run as a service</h2>
|
||||
<p class="mb-4">For always-on availability, run clavitor as a systemd service.</p>
|
||||
<p class="mb-4">For always-on availability, run Clavitor as a systemd service.</p>
|
||||
<p class="label mb-3">/etc/systemd/system/clavitor.service</p>
|
||||
<div class="code-block mb-4"><pre>[Unit]
|
||||
Description=clavitor
|
||||
|
|
@ -109,7 +105,7 @@ WantedBy=multi-user.target</pre></div>
|
|||
<div class="code-block mb-8"><span class="prompt">$</span> sudo systemctl enable --now clavitor</div>
|
||||
|
||||
<h2 class="mb-4">Expose to the internet</h2>
|
||||
<p class="mb-4">Put clavitor behind Caddy for TLS and remote access.</p>
|
||||
<p class="mb-4">Put Clavitor behind Caddy for TLS and remote access.</p>
|
||||
<p class="label mb-3">Caddyfile</p>
|
||||
<div class="code-block"><pre>vault.yourdomain.com {
|
||||
reverse_proxy localhost:1984
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
{{define "noc"}}
|
||||
<div class="hero container">
|
||||
<p class="label accent mb-4">Network Operations</p>
|
||||
<h1 class="mb-4">Clavitor NOC</h1>
|
||||
<p class="lead">Real-time telemetry from {{len .Pops}} points of presence.</p>
|
||||
</div>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<div class="section container">
|
||||
<div id="noc-summary" class="glass-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:24px">
|
||||
<div class="glass-pop"><div class="glass-header"><span class="glass-city">Nodes reporting</span></div><div class="glass-val" style="font-size:1.5rem;font-weight:700" id="s-nodes">—</div></div>
|
||||
<div class="glass-pop"><div class="glass-header"><span class="glass-city">Avg CPU</span></div><div class="glass-val" style="font-size:1.5rem;font-weight:700" id="s-cpu">—</div></div>
|
||||
<div class="glass-pop"><div class="glass-header"><span class="glass-city">Avg Mem</span></div><div class="glass-val" style="font-size:1.5rem;font-weight:700" id="s-mem">—</div></div>
|
||||
<div class="glass-pop"><div class="glass-header"><span class="glass-city">Total Vaults</span></div><div class="glass-val" style="font-size:1.5rem;font-weight:700" id="s-vaults">—</div></div>
|
||||
</div>
|
||||
<div id="noc-error" style="display:none;color:var(--brand-red);font-size:0.85rem;margin-bottom:16px"></div>
|
||||
<div id="noc-cards" class="glass-grid" style="grid-template-columns:repeat(3,1fr)"></div>
|
||||
<p class="mt-4 text-sm text-tertiary" id="noc-updated">Loading...</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "noc-script"}}
|
||||
<script>
|
||||
(function() {
|
||||
const pin = new URLSearchParams(window.location.search).get('pin') || '';
|
||||
const API = '/noc';
|
||||
const P = '?pin=' + encodeURIComponent(pin);
|
||||
function apiUrl(path, extra) { return API + path + P + (extra ? '&' + extra : ''); }
|
||||
const REFRESH_MS = 30000;
|
||||
const history = {};
|
||||
|
||||
function colorClass(pct, w=60, c=85) { return pct >= c ? 'glass-slow' : pct >= w ? 'glass-ok' : 'glass-fast'; }
|
||||
function barColor(pct, w=60, c=85) { return pct >= c ? 'var(--brand-red)' : pct >= w ? '#ca8a04' : '#16a34a'; }
|
||||
function fmtUptime(s) {
|
||||
if (!s) return '—';
|
||||
const d=Math.floor(s/86400), h=Math.floor((s%86400)/3600), m=Math.floor((s%3600)/60);
|
||||
return d > 0 ? d+'d '+h+'h' : h > 0 ? h+'h '+m+'m' : m+'m';
|
||||
}
|
||||
function fmtAgo(ts) {
|
||||
const s = Math.round(Date.now()/1000 - ts);
|
||||
if (s < 5) return 'just now';
|
||||
if (s < 60) return s+'s ago';
|
||||
if (s < 3600) return Math.floor(s/60)+'m ago';
|
||||
return Math.floor(s/3600)+'h ago';
|
||||
}
|
||||
function drawSpark(canvas, points, key, color) {
|
||||
const W = canvas.clientWidth || 320, H = 40;
|
||||
canvas.width = W * devicePixelRatio; canvas.height = H * devicePixelRatio;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
if (!points || points.length < 2) return;
|
||||
const vals = points.map(p => p[key]);
|
||||
const max = Math.max(...vals, 5);
|
||||
const xs = i => (i / (points.length-1)) * W;
|
||||
const ys = v => H - 2 - (v/max) * (H-4);
|
||||
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 1.5;
|
||||
ctx.moveTo(xs(0), ys(vals[0]));
|
||||
for (let i=1;i<vals.length;i++) ctx.lineTo(xs(i), ys(vals[i]));
|
||||
ctx.stroke();
|
||||
ctx.lineTo(xs(vals.length-1), H); ctx.lineTo(xs(0), H); ctx.closePath();
|
||||
ctx.fillStyle = color.replace(')', ',0.08)').replace('rgb','rgba');
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function renderCard(t, hist) {
|
||||
if (t._pending) return `
|
||||
<div class="glass-pop" style="opacity:.5">
|
||||
<div class="glass-header"><span class="glass-city">${t.node_id}</span><span class="glass-status glass-status-planned">PENDING</span></div>
|
||||
<div style="color:var(--muted);font-size:0.8rem;text-align:center;padding:20px 0">Awaiting telemetry</div>
|
||||
</div>`;
|
||||
const ageS = Math.round(Date.now()/1000 - t.received_at);
|
||||
const stale = ageS > 150, offline = ageS > 300;
|
||||
const memPct = t.memory_total_mb ? Math.round(t.memory_used_mb/t.memory_total_mb*100) : 0;
|
||||
const diskPct = t.disk_total_mb ? Math.round(t.disk_used_mb/t.disk_total_mb*100) : 0;
|
||||
const statusClass = offline ? 'glass-status-planned' : stale ? 'glass-status-planned' : 'glass-status-live';
|
||||
const statusText = offline ? 'OFFLINE' : stale ? 'STALE' : 'LIVE';
|
||||
const borderColor = offline ? 'var(--brand-red)' : stale ? '#ca8a04' : '#16a34a';
|
||||
return `
|
||||
<div class="glass-pop" style="border-left:3px solid ${borderColor}">
|
||||
<div class="glass-header">
|
||||
<span class="glass-city">${t.node_id}</span>
|
||||
<span class="glass-status ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div style="font-size:0.72rem;color:var(--muted);margin-bottom:10px">${t.hostname || ''} · v${t.version || ''}</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
|
||||
<div><div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em">CPU</div><div class="${colorClass(t.cpu_percent)}" style="font-size:1.3rem;font-weight:700">${t.cpu_percent.toFixed(1)}%</div><div style="font-size:0.7rem;color:var(--muted)">load ${t.load_1m.toFixed(2)}</div></div>
|
||||
<div><div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em">Memory</div><div class="${colorClass(memPct)}" style="font-size:1.3rem;font-weight:700">${memPct}%</div><div style="font-size:0.7rem;color:var(--muted)">${t.memory_used_mb} / ${t.memory_total_mb} MB</div></div>
|
||||
</div>
|
||||
<div style="margin-bottom:8px"><div style="display:flex;justify-content:space-between;font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:3px"><span>Mem</span><span>${memPct}%</span></div><div style="background:var(--border);border-radius:2px;height:4px"><div style="height:4px;border-radius:2px;width:${memPct}%;background:${barColor(memPct)}"></div></div></div>
|
||||
<div style="margin-bottom:8px"><div style="display:flex;justify-content:space-between;font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:3px"><span>Disk</span><span>${diskPct}% · ${t.disk_used_mb} / ${t.disk_total_mb} MB</span></div><div style="background:var(--border);border-radius:2px;height:4px"><div style="height:4px;border-radius:2px;width:${diskPct}%;background:${barColor(diskPct,70,90)}"></div></div></div>
|
||||
<div style="border-top:1px solid var(--border);padding-top:10px;margin-top:8px"><div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px">CPU % — last ${hist ? hist.length : 0} samples</div><canvas class="noc-spark" id="spark-cpu-${t.node_id}" style="width:100%;height:40px;display:block"></canvas></div>
|
||||
<div style="margin-top:8px"><div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px">Mem % — last ${hist ? hist.length : 0} samples</div><canvas class="noc-spark" id="spark-mem-${t.node_id}" style="width:100%;height:40px;display:block"></canvas></div>
|
||||
<div style="display:flex;justify-content:space-between;margin-top:10px;padding-top:8px;border-top:1px solid var(--border);font-size:0.7rem;color:var(--muted)">
|
||||
<span>↑ ${fmtUptime(t.uptime_seconds)}</span>
|
||||
<span style="color:var(--brand-red);font-weight:600">◈ ${t.vault_count} vaults · ${t.vault_size_mb.toFixed(1)} MB</span>
|
||||
<span title="${new Date(t.received_at*1000).toISOString()}">⏱ ${fmtAgo(t.received_at)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function fetchHistory(nodeId) {
|
||||
try {
|
||||
const r = await fetch(apiUrl('/api/telemetry/history', 'node='+nodeId+'&limit=60'), {cache:'no-cache'});
|
||||
const d = await r.json();
|
||||
if (d.history) history[nodeId] = d.history.map(h => ({ts:h.ts, cpu:h.cpu, mem_pct: h.mem_total_mb ? Math.round(h.mem_used_mb/h.mem_total_mb*100) : 0}));
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [tRes, nRes] = await Promise.all([
|
||||
fetch(apiUrl('/api/telemetry'), {cache:'no-cache'}),
|
||||
fetch(apiUrl('/api/nodes'), {cache:'no-cache'}),
|
||||
]);
|
||||
const tData = await tRes.json(), nData = await nRes.json();
|
||||
const tMap = {};
|
||||
for (const t of (tData.telemetry || [])) tMap[t.node_id] = t;
|
||||
const liveIds = (nData.nodes || []).filter(n => n.Status === 'live').map(n => n.ID);
|
||||
const nodes = liveIds.map(id => tMap[id] || {node_id:id, _pending:true});
|
||||
document.getElementById('noc-error').style.display = 'none';
|
||||
|
||||
document.getElementById('s-nodes').textContent = nodes.length;
|
||||
if (nodes.length) {
|
||||
const live = nodes.filter(n => !n._pending);
|
||||
const avgCPU = live.reduce((a,n)=>a+n.cpu_percent,0)/live.length;
|
||||
const avgMem = live.reduce((a,n)=>a+(n.memory_total_mb?n.memory_used_mb/n.memory_total_mb*100:0),0)/live.length;
|
||||
const totalVaults = live.reduce((a,n)=>a+n.vault_count,0);
|
||||
document.getElementById('s-cpu').textContent = avgCPU.toFixed(1)+'%';
|
||||
document.getElementById('s-mem').textContent = avgMem.toFixed(1)+'%';
|
||||
document.getElementById('s-vaults').textContent = totalVaults;
|
||||
}
|
||||
|
||||
await Promise.all(nodes.map(n => fetchHistory(n.node_id)));
|
||||
document.getElementById('noc-cards').innerHTML = nodes.map(t => renderCard(t, history[t.node_id])).join('');
|
||||
nodes.forEach(t => {
|
||||
const hist = history[t.node_id] || [];
|
||||
const cpu = document.getElementById('spark-cpu-'+t.node_id);
|
||||
const mem = document.getElementById('spark-mem-'+t.node_id);
|
||||
if (cpu) drawSpark(cpu, hist, 'cpu', 'rgb(220,38,38)');
|
||||
if (mem) drawSpark(mem, hist, 'mem_pct', 'rgb(10,10,10)');
|
||||
});
|
||||
document.getElementById('noc-updated').textContent = 'Updated ' + new Date().toLocaleTimeString();
|
||||
} catch(e) {
|
||||
const err = document.getElementById('noc-error');
|
||||
err.textContent = 'Fetch error: ' + e.message;
|
||||
err.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, REFRESH_MS);
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
@ -13,10 +13,10 @@
|
|||
<div class="price-card">
|
||||
<p class="label mb-4">Self-hosted</p>
|
||||
<div class="price-amount mb-2">Free</div>
|
||||
<p class="mb-6">Forever. MIT license. No strings.</p>
|
||||
<p class="mb-6">Forever. Elastic License 2.0. No strings.</p>
|
||||
<a href="/install" class="btn btn-ghost btn-block mb-8">Self-host guide →</a>
|
||||
<p class="label mb-4">What you get</p>
|
||||
<ul class="checklist"><li>Three-tier encryption (Vault, Credential, Identity)</li><li>WebAuthn PRF (Identity biometric encryption)</li><li>MCP server for AI agents</li><li>Scoped MCP tokens (multi-agent)</li><li>TOTP generation via MCP</li><li>Browser extension (Chrome, Firefox)</li><li>Import from Bitwarden / 1Password</li><li>LLM-powered field classification</li><li>Unlimited entries</li><li>Full source code (MIT)</li></ul>
|
||||
<ul class="checklist"><li>Three-tier encryption (Vault, Credential, Identity)</li><li>WebAuthn PRF (Identity biometric encryption)</li><li>CLI for AI agents (encrypted delivery)</li><li>Scoped agent tokens (multi-agent)</li><li>TOTP generation via CLI</li><li>Browser extension (Chrome, Firefox)</li><li>Import from Bitwarden / 1Password</li><li>LLM-powered field classification</li><li>Unlimited entries</li><li>Full source code (ELv2)</li></ul>
|
||||
</div>
|
||||
|
||||
<div class="price-card featured">
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
</ul>
|
||||
|
||||
<h2>What this policy covers</h2>
|
||||
<p>This privacy policy applies to the hosted clavitor service at clavitor.com. If you self-host clavitor, your data never touches our servers and this policy doesn't apply to you — your privacy is entirely in your own hands.</p>
|
||||
<p>This privacy policy applies to the hosted Clavitor service at clavitor.com. If you self-host Clavitor, your data never touches our servers and this policy doesn't apply to you — your privacy is entirely in your own hands.</p>
|
||||
|
||||
<h2>Data we store</h2>
|
||||
<p>When you use hosted clavitor, we store:</p>
|
||||
<p>When you use hosted Clavitor, we store:</p>
|
||||
<ul>
|
||||
<li><strong>Account information:</strong> email address and authentication credentials</li>
|
||||
<li><strong>Credential fields:</strong> encrypted at rest with AES-256-GCM using your vault key</li>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="grid-2">
|
||||
|
||||
<div class="card">
|
||||
<p class="mb-4">"I tried giving Claude access to 1Password and it immediately wanted to read my credit card details. That's not what I wanted. clavitor is the only thing that solves this properly."</p>
|
||||
<p class="mb-4">"I tried giving Claude access to 1Password and it immediately wanted to read my credit card details. That's not what I wanted. Clavitor is the only thing that solves this properly."</p>
|
||||
<p class="label">@devrel_mike · X · 2024</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="mb-4">"clavitor LLM field mapping matches by intent. Entries are indexed by URL — the right credential for the right site, every time."</p>
|
||||
<p class="mb-4">"Clavitor LLM field mapping matches by intent. Entries are indexed by URL — the right credential for the right site, every time."</p>
|
||||
<p class="label">@jolaneti11 · X · 2024</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<div class="container" style="padding-top:80px">
|
||||
<div style="padding: 5rem 0 2rem">
|
||||
<p class="label accent mb-3">Design System</p>
|
||||
<h1>clavitor Styleguide</h1>
|
||||
<h1>Clavitor Styleguide</h1>
|
||||
<p class="lead mt-4">Single source of truth. One stylesheet: <code style="font-family:var(--font-mono);color:var(--accent)">clavitor.css</code>. No inline styles in HTML.</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@
|
|||
<div class="prose prose-width">
|
||||
|
||||
<h2>1. Acceptance</h2>
|
||||
<p>By using clavitor (the "Service"), you agree to these terms. If you don't agree, don't use the Service.</p>
|
||||
<p>By using Clavitor (the "Service"), you agree to these terms. If you don't agree, don't use the Service.</p>
|
||||
|
||||
<h2>2. Description</h2>
|
||||
<p>clavitor is a password manager with three-tier encryption: Vault Encryption (at rest), Credential Encryption (per-field, server-side), and Identity Encryption (per-field, client-side). The hosted service stores encrypted vault data on your behalf. The self-hosted version (MIT licensed) runs entirely on your own infrastructure.</p>
|
||||
<p>Clavitor is a password manager with three-tier encryption: Vault Encryption (at rest), Credential Encryption (per-field, server-side), and Identity Encryption (per-field, client-side). The hosted service stores encrypted vault data on your behalf. The self-hosted version (Elastic License 2.0) runs entirely on your own infrastructure.</p>
|
||||
|
||||
<h2>3. Accounts</h2>
|
||||
<p>You are responsible for maintaining the security of your account credentials and authenticator device. We cannot recover Identity fields if you lose access to your WebAuthn authenticator — the mathematical design prevents it.</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue