Initial commit: clavitor project structure (migrated from vault1984)

This commit is contained in:
James 2026-03-22 05:32:24 -04:00
commit 5597bbc6b5
507 changed files with 26644 additions and 0 deletions

View File

@ -0,0 +1,802 @@
/* clavitor account — 2027 */
:root {
--bg: #060a10;
--bg-subtle: #0a1018;
--surface: rgba(255,255,255,0.03);
--surface-hover: rgba(255,255,255,0.06);
--surface-active: rgba(255,255,255,0.08);
--glass: rgba(12,18,30,0.7);
--glass-border: rgba(255,255,255,0.06);
--glass-border-hover: rgba(255,255,255,0.12);
--text: #f1f5f9;
--text-secondary: #b0bec5;
--text-tertiary: #8a9bb0;
--accent: #4ade80;
--accent-dim: rgba(74,222,128,0.12);
--accent-glow: rgba(74,222,128,0.25);
--gold: #D4AF37;
--gold-dim: rgba(212,175,55,0.12);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.12);
--radius: 1rem;
--radius-sm: 0.625rem;
--radius-xs: 0.375rem;
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--mono: 'JetBrains Mono', 'SF Mono', monospace;
--ease: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
/* ── Ambient background ── */
.ambient {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
}
.ambient::before {
content: '';
position: absolute;
width: 800px;
height: 800px;
top: -200px;
right: -200px;
background: radial-gradient(circle, rgba(74,222,128,0.04) 0%, transparent 70%);
animation: drift 25s ease-in-out infinite alternate;
}
.ambient::after {
content: '';
position: absolute;
width: 600px;
height: 600px;
bottom: -100px;
left: -150px;
background: radial-gradient(circle, rgba(212,175,55,0.03) 0%, transparent 70%);
animation: drift 30s ease-in-out infinite alternate-reverse;
}
@keyframes drift {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(60px, 40px) scale(1.1); }
}
/* ── Grid lines (subtle background texture) ── */
.gridlines {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 20%, transparent 100%);
}
/* ── Layout ── */
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 1.5rem;
}
.container-narrow {
max-width: 480px;
}
/* ── Topbar ── */
.topbar {
position: sticky;
top: 0;
z-index: 100;
padding: 0.875rem 0;
background: rgba(6,10,16,0.8);
backdrop-filter: blur(20px) saturate(1.2);
-webkit-backdrop-filter: blur(20px) saturate(1.2);
border-bottom: 1px solid var(--glass-border);
}
.topbar-inner {
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar-brand {
font-family: var(--mono);
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
text-decoration: none;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 0.5rem;
}
.topbar-brand .port {
color: var(--text-tertiary);
font-weight: 400;
}
.topbar-nav {
display: flex;
align-items: center;
gap: 0.25rem;
}
.topbar-nav a {
font-size: 0.8125rem;
color: var(--text-secondary);
text-decoration: none;
padding: 0.375rem 0.75rem;
border-radius: var(--radius-xs);
transition: all 0.2s var(--ease);
}
.topbar-nav a:hover { color: var(--text); background: var(--surface-hover); }
.topbar-nav a.active { color: var(--text); background: var(--surface-active); }
.topbar-nav .separator {
width: 1px;
height: 1rem;
background: var(--glass-border);
margin: 0 0.375rem;
}
/* ── Typography ── */
h1 {
font-size: clamp(1.75rem, 3.5vw, 2.5rem);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.15;
}
h2 {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.3;
}
h3 {
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.label {
font-family: var(--mono);
font-size: 0.6875rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
}
.label.accent { color: var(--accent); }
.label.gold { color: var(--gold); }
.label.red { color: var(--red); }
.mono { font-family: var(--mono); }
.text-secondary { color: var(--text-secondary); }
.text-accent { color: var(--accent); }
.text-gold { color: var(--gold); }
/* ── Buttons ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-family: var(--font);
font-size: 0.875rem;
font-weight: 550;
padding: 0.6875rem 1.375rem;
border-radius: var(--radius-sm);
border: none;
cursor: pointer;
text-decoration: none;
transition: all 0.25s var(--ease);
outline: none;
position: relative;
overflow: hidden;
}
.btn:focus-visible {
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
}
.btn-primary {
background: var(--accent);
color: #0a0f14;
}
.btn-primary:hover {
background: #6ee7a0;
transform: translateY(-1px);
box-shadow: 0 4px 20px var(--accent-glow);
}
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
background: var(--surface);
color: var(--text-secondary);
border: 1px solid var(--glass-border);
}
.btn-ghost:hover {
color: var(--text);
background: var(--surface-hover);
border-color: var(--glass-border-hover);
}
.btn-danger {
background: var(--red-dim);
color: var(--red);
border: 1px solid rgba(239,68,68,0.15);
}
.btn-danger:hover {
background: rgba(239,68,68,0.18);
border-color: rgba(239,68,68,0.3);
}
.btn-block { width: 100%; }
.btn-lg {
padding: 0.875rem 1.75rem;
font-size: 0.9375rem;
}
.btn .icon {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
/* ── Cards ── */
.card {
background: var(--surface);
border: 1px solid var(--glass-border);
border-radius: var(--radius);
padding: 1.5rem;
transition: all 0.3s var(--ease);
}
.card-interactive {
cursor: pointer;
}
.card-interactive:hover {
background: var(--surface-hover);
border-color: var(--glass-border-hover);
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.card-interactive:active { transform: translateY(0); }
.card-glow:hover {
box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 0 1px var(--accent-dim);
}
.card-gold {
border-color: rgba(212,175,55,0.15);
}
.card-gold:hover {
border-color: rgba(212,175,55,0.3);
box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 0 1px var(--gold-dim);
}
/* ── Glass panels ── */
.glass {
background: var(--glass);
backdrop-filter: blur(24px) saturate(1.3);
-webkit-backdrop-filter: blur(24px) saturate(1.3);
border: 1px solid var(--glass-border);
border-radius: var(--radius);
}
/* ── Forms ── */
.field { margin-bottom: 1.25rem; }
.field label {
display: block;
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.input {
width: 100%;
font-family: var(--font);
font-size: 0.9375rem;
padding: 0.75rem 1rem;
background: var(--surface);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
color: var(--text);
outline: none;
transition: all 0.2s var(--ease);
}
.input::placeholder { color: var(--text-secondary); }
.input:hover { border-color: var(--glass-border-hover); }
.input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.input-mono {
font-family: var(--mono);
font-size: 0.875rem;
letter-spacing: 0.02em;
}
/* ── Status indicators ── */
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.live {
background: var(--accent);
box-shadow: 0 0 8px var(--accent-glow);
animation: pulse-live 2s ease-in-out infinite;
}
.status-dot.expired {
background: var(--red);
box-shadow: 0 0 8px rgba(239,68,68,0.3);
}
.status-dot.pending {
background: var(--gold);
box-shadow: 0 0 8px rgba(212,175,55,0.3);
animation: pulse-live 2s ease-in-out infinite;
}
@keyframes pulse-live {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ── Region cards ── */
.region-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
}
.region-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.125rem;
}
.region-card .region-name {
font-weight: 600;
font-size: 0.9375rem;
}
.region-card .region-location {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.region-card .region-flag {
font-size: 1.25rem;
line-height: 1;
}
.region-card.selected {
border-color: var(--accent);
background: var(--accent-dim);
box-shadow: 0 0 0 1px var(--accent-dim);
}
.region-card.hq {
border-color: rgba(212,175,55,0.2);
}
.region-card.hq .label { color: var(--gold); }
/* ── Vault list ── */
.vault-row {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--glass-border);
transition: background 0.15s var(--ease);
}
.vault-row:last-child { border-bottom: none; }
.vault-row:hover { background: var(--surface-hover); }
.vault-id {
font-family: var(--mono);
font-size: 0.875rem;
font-weight: 500;
color: var(--text);
}
.vault-region {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.vault-meta {
margin-left: auto;
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.8125rem;
color: var(--text-secondary);
}
/* ── Dividers ── */
.divider {
border: none;
height: 1px;
background: var(--glass-border);
margin: 2rem 0;
}
/* ── Section spacing ── */
.section {
padding: 3rem 0;
}
.section-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
/* ── Login page ── */
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
}
.login-card {
width: 100%;
max-width: 400px;
padding: 2.5rem;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header .brand {
font-family: var(--mono);
font-size: 1.125rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
}
.login-header p {
color: var(--text-secondary);
font-size: 0.875rem;
}
.login-divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 1.5rem 0;
color: var(--text-secondary);
font-size: 0.75rem;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--glass-border);
}
.login-footer {
text-align: center;
margin-top: 1.5rem;
font-size: 0.8125rem;
color: var(--text-secondary);
}
.login-footer a {
color: var(--accent);
text-decoration: none;
}
.login-footer a:hover { text-decoration: underline; }
/* ── Checkout ── */
.checkout-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
}
.checkout-card {
width: 100%;
max-width: 480px;
padding: 2.5rem;
}
.price-display {
display: flex;
align-items: baseline;
gap: 0.375rem;
margin: 1rem 0;
}
.price-amount {
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.03em;
color: var(--text);
}
.price-period {
font-size: 1rem;
color: var(--text-secondary);
}
.price-note {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.feature-list {
list-style: none;
margin: 1.5rem 0;
}
.feature-list li {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0;
font-size: 0.875rem;
color: var(--text-secondary);
}
.feature-list .check {
color: var(--accent);
font-size: 0.75rem;
flex-shrink: 0;
}
/* ── Settings ── */
.settings-section {
padding: 1.5rem 0;
}
.settings-section + .settings-section {
border-top: 1px solid var(--glass-border);
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.settings-label {
font-size: 0.9375rem;
font-weight: 500;
}
.settings-desc {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
/* ── Danger zone ── */
.danger-zone {
border: 1px solid rgba(239,68,68,0.15);
border-radius: var(--radius);
padding: 1.5rem;
margin-top: 1rem;
}
.danger-zone h3 {
color: var(--red);
margin-bottom: 0.5rem;
}
.danger-zone p {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
/* ── Passkey button ── */
.btn-passkey {
background: linear-gradient(135deg, rgba(74,222,128,0.1) 0%, rgba(74,222,128,0.05) 100%);
color: var(--accent);
border: 1px solid rgba(74,222,128,0.2);
font-weight: 600;
}
.btn-passkey:hover {
background: linear-gradient(135deg, rgba(74,222,128,0.15) 0%, rgba(74,222,128,0.08) 100%);
border-color: rgba(74,222,128,0.35);
box-shadow: 0 4px 20px rgba(74,222,128,0.1);
transform: translateY(-1px);
}
.btn-passkey:active { transform: translateY(0); }
.passkey-icon {
width: 1.25rem;
height: 1.25rem;
}
/* ── Toast / notification ── */
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.875rem 1.25rem;
border-radius: var(--radius-sm);
font-size: 0.8125rem;
font-weight: 500;
transform: translateY(120%);
opacity: 0;
transition: all 0.35s var(--ease);
z-index: 200;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
.toast.success {
background: var(--accent-dim);
color: var(--accent);
border: 1px solid rgba(74,222,128,0.2);
}
.toast.error {
background: var(--red-dim);
color: var(--red);
border: 1px solid rgba(239,68,68,0.2);
}
/* ── Skeleton loading ── */
.skeleton {
background: linear-gradient(90deg, var(--surface) 25%, var(--surface-hover) 50%, var(--surface) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: var(--radius-xs);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* ── Responsive ── */
@media (max-width: 640px) {
.container { padding: 0 1rem; }
.login-card, .checkout-card { padding: 1.75rem; }
.region-grid { grid-template-columns: 1fr; }
.vault-row { flex-wrap: wrap; }
.vault-meta { margin-left: 0; width: 100%; justify-content: flex-start; }
.settings-row { flex-direction: column; align-items: flex-start; gap: 0.75rem; }
.topbar-nav { gap: 0; }
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--glass-border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.12); }
/* ── Selection ── */
::selection { background: var(--accent-dim); color: var(--text); }
/* ── Focus management ── */
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ── Transitions for page content ── */
.fade-in {
animation: fadeIn 0.4s var(--ease-out) both;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in-delay-1 { animation-delay: 0.05s; }
.fade-in-delay-2 { animation-delay: 0.1s; }
.fade-in-delay-3 { animation-delay: 0.15s; }

Binary file not shown.

Binary file not shown.

Binary file not shown.

306
commercial/account/api.go Normal file
View File

@ -0,0 +1,306 @@
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"log"
"net/http"
"strings"
"time"
)
func randomToken(n int) string {
b := make([]byte, n)
rand.Read(b)
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
}
func jsonResp(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func jsonErr(w http.ResponseWriter, status int, msg string) {
jsonResp(w, status, map[string]string{"error": msg})
}
// Get email from session cookie
func authEmail(r *http.Request) string {
c, err := r.Cookie("v1984_session")
if err != nil {
return ""
}
email, err := sessionGet(c.Value)
if err != nil {
return ""
}
return email
}
func setSessionCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: "v1984_session",
Value: token,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 86400,
})
}
func clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "v1984_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
})
}
// POST /api/auth/email — send login code (stubbed: always 123456)
func apiAuthEmail(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" {
jsonErr(w, 400, "email required")
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
// Auto-create account if it doesn't exist
exists, _, _ := accountGet(email)
if !exists {
if err := accountCreate(email); err != nil {
log.Printf("account auto-create error: %v", err)
jsonErr(w, 500, "internal error")
return
}
log.Printf("account auto-created: %s", email)
}
// Store code (stub: always 123456)
code := "123456"
if err := loginCodeSet(email, code); err != nil {
log.Printf("login code error: %v", err)
jsonErr(w, 500, "internal error")
return
}
log.Printf("login code for %s: %s (stub — not emailed)", email, code)
jsonResp(w, 200, map[string]string{"status": "sent"})
}
// POST /api/auth/verify — verify login code
func apiAuthVerify(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
var req struct {
Email string `json:"email"`
Code string `json:"code"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" || req.Code == "" {
jsonErr(w, 400, "email and code required")
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
ok, err := loginCodeVerify(email, req.Code)
if err != nil || !ok {
jsonErr(w, 401, "invalid or expired code")
return
}
token, err := sessionCreate(email)
if err != nil {
jsonErr(w, 500, "internal error")
return
}
setSessionCookie(w, token)
jsonResp(w, 200, map[string]string{"status": "ok"})
}
// POST /api/auth/logout
func apiAuthLogout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("v1984_session"); err == nil {
sessionDelete(c.Value)
}
clearSessionCookie(w)
jsonResp(w, 200, map[string]string{"status": "ok"})
}
// POST /api/checkout — create account (stub: no Stripe yet)
func apiCheckout(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" {
jsonErr(w, 400, "email required")
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
// Create account (idempotent)
if err := accountCreate(email); err != nil {
log.Printf("account create error: %v", err)
jsonErr(w, 500, "internal error")
return
}
// Auto-login after checkout (stub — real flow goes through Stripe first)
token, err := sessionCreate(email)
if err != nil {
jsonErr(w, 500, "internal error")
return
}
setSessionCookie(w, token)
log.Printf("account created (stub): %s", email)
// In production: return Stripe checkout URL
// For now: redirect to regions page
jsonResp(w, 200, map[string]string{"url": basePath + "/regions"})
}
// GET /api/vaults — list vaults for authenticated user
func apiVaults(w http.ResponseWriter, r *http.Request) {
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
vaults, err := vaultList(email)
if err != nil {
jsonErr(w, 500, "internal error")
return
}
if vaults == nil {
vaults = []Vault{}
}
count, _ := vaultCount(email)
jsonResp(w, 200, map[string]any{
"email": email,
"vaults": vaults,
"count": count,
"max": 1, // consumer: 1 vault
})
}
// POST /api/vault/create
func apiVaultCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
var req struct {
VaultID string `json:"vault_id"`
Region string `json:"region"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Region == "" {
jsonErr(w, 400, "region required")
return
}
// Check capacity
count, _ := vaultCount(email)
if count >= 1 {
jsonErr(w, 409, "vault limit reached")
return
}
// Generate vault ID if not provided (normally comes from PRF on the POP)
vaultID := req.VaultID
if vaultID == "" {
vaultID = randomToken(4)[:6] // 6 chars, stub
}
if err := vaultCreate(vaultID, email, req.Region); err != nil {
log.Printf("vault create error: %v", err)
jsonErr(w, 500, "internal error")
return
}
log.Printf("vault created: %s for %s in %s", vaultID, email, req.Region)
jsonResp(w, 201, map[string]string{
"vault_id": vaultID,
"region": req.Region,
"expires_at": time.Now().AddDate(1, 0, 0).UTC().Format(time.RFC3339),
})
}
// POST /api/vault/{id}/delete
func apiVaultDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
// Extract vault ID from path: /api/vault/{id}/delete
parts := strings.Split(r.URL.Path, "/")
var vaultID string
for i, p := range parts {
if p == "vault" && i+1 < len(parts) {
vaultID = parts[i+1]
break
}
}
if vaultID == "" {
jsonErr(w, 400, "vault_id required")
return
}
if err := vaultDelete(vaultID, email); err != nil {
jsonErr(w, 404, "vault not found")
return
}
log.Printf("vault deleted: %s for %s", vaultID, email)
jsonResp(w, 200, map[string]string{"status": "deleted"})
}
// GET /api/account — account info
func apiAccount(w http.ResponseWriter, r *http.Request) {
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
exists, stripeID, _ := accountGet(email)
if !exists {
jsonErr(w, 404, "account not found")
return
}
count, _ := vaultCount(email)
jsonResp(w, 200, map[string]any{
"email": email,
"stripe_id": stripeID,
"vaults": count,
"max": 1,
})
}

192
commercial/account/db.go Normal file
View File

@ -0,0 +1,192 @@
package main
import (
"database/sql"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func initDB(path string) {
var err error
db, err = sql.Open("sqlite3", path+"?_journal=WAL&_busy_timeout=5000")
if err != nil {
log.Fatalf("db open: %v", err)
}
migrations := []string{
`CREATE TABLE IF NOT EXISTS accounts (
email TEXT PRIMARY KEY,
stripe_id TEXT DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)`,
`CREATE TABLE IF NOT EXISTS vaults (
vault_id TEXT PRIMARY KEY,
account_email TEXT NOT NULL REFERENCES accounts(email),
region TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)`,
`CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
email TEXT NOT NULL REFERENCES accounts(email),
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)`,
`CREATE TABLE IF NOT EXISTS login_codes (
email TEXT PRIMARY KEY,
code TEXT NOT NULL,
expires_at TEXT NOT NULL
)`,
}
for _, m := range migrations {
if _, err := db.Exec(m); err != nil {
log.Fatalf("migration: %v", err)
}
}
log.Println(" database ready")
}
// Accounts
func accountGet(email string) (bool, string, error) {
var stripeID string
err := db.QueryRow("SELECT stripe_id FROM accounts WHERE email = ?", email).Scan(&stripeID)
if err == sql.ErrNoRows {
return false, "", nil
}
if err != nil {
return false, "", err
}
return true, stripeID, nil
}
func accountCreate(email string) error {
_, err := db.Exec("INSERT OR IGNORE INTO accounts (email) VALUES (?)", email)
return err
}
// Vaults
func vaultList(email string) ([]Vault, error) {
rows, err := db.Query(
"SELECT vault_id, region, expires_at, created_at FROM vaults WHERE account_email = ? ORDER BY created_at",
email,
)
if err != nil {
return nil, err
}
defer rows.Close()
var vaults []Vault
for rows.Next() {
var v Vault
if err := rows.Scan(&v.ID, &v.Region, &v.ExpiresAt, &v.CreatedAt); err != nil {
return nil, err
}
vaults = append(vaults, v)
}
return vaults, nil
}
func vaultCount(email string) (int, error) {
var n int
err := db.QueryRow("SELECT COUNT(*) FROM vaults WHERE account_email = ?", email).Scan(&n)
return n, err
}
func vaultCreate(id, email, region string) error {
expires := time.Now().AddDate(1, 0, 0).UTC().Format(time.RFC3339)
_, err := db.Exec(
"INSERT INTO vaults (vault_id, account_email, region, expires_at) VALUES (?, ?, ?, ?)",
id, email, region, expires,
)
return err
}
func vaultDelete(id, email string) error {
res, err := db.Exec("DELETE FROM vaults WHERE vault_id = ? AND account_email = ?", id, email)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return sql.ErrNoRows
}
return nil
}
// Sessions
func sessionCreate(email string) (string, error) {
token := randomToken(32)
expires := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)
_, err := db.Exec(
"INSERT INTO sessions (token, email, expires_at) VALUES (?, ?, ?)",
token, email, expires,
)
return token, err
}
func sessionGet(token string) (string, error) {
var email string
var expires string
err := db.QueryRow("SELECT email, expires_at FROM sessions WHERE token = ?", token).Scan(&email, &expires)
if err != nil {
return "", err
}
t, _ := time.Parse(time.RFC3339, expires)
if time.Now().After(t) {
db.Exec("DELETE FROM sessions WHERE token = ?", token)
return "", sql.ErrNoRows
}
return email, nil
}
func sessionDelete(token string) {
db.Exec("DELETE FROM sessions WHERE token = ?", token)
}
// Login codes
func loginCodeSet(email, code string) error {
expires := time.Now().Add(10 * time.Minute).UTC().Format(time.RFC3339)
_, err := db.Exec(
"INSERT OR REPLACE INTO login_codes (email, code, expires_at) VALUES (?, ?, ?)",
email, code, expires,
)
return err
}
func loginCodeVerify(email, code string) (bool, error) {
var stored string
var expires string
err := db.QueryRow("SELECT code, expires_at FROM login_codes WHERE email = ?", email).Scan(&stored, &expires)
if err != nil {
return false, nil
}
t, _ := time.Parse(time.RFC3339, expires)
if time.Now().After(t) {
db.Exec("DELETE FROM login_codes WHERE email = ?", email)
return false, nil
}
if stored != code {
return false, nil
}
db.Exec("DELETE FROM login_codes WHERE email = ?", email)
return true, nil
}
// Types
type Vault struct {
ID string `json:"vault_id"`
Region string `json:"region"`
ExpiresAt string `json:"expires_at"`
CreatedAt string `json:"created_at"`
}

View File

@ -0,0 +1,5 @@
<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>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,5 @@
module clavitor.com/account
go 1.23.6
require github.com/mattn/go-sqlite3 v1.14.37 // indirect

View File

@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

202
commercial/account/main.go Normal file
View File

@ -0,0 +1,202 @@
package main
import (
"embed"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
//go:embed templates/*.tmpl account.css favicon.svg
var embedded embed.FS
var devMode bool
var basePath string
type PageData struct {
Page string
Title string
Desc string
ActiveNav string
Base string
Data any
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8098"
}
devMode = os.Getenv("DEV") == "1"
basePath = strings.TrimRight(os.Getenv("BASE_PATH"), "/")
dbPath := os.Getenv("DB_PATH")
if dbPath == "" {
dbPath = "account.db"
}
initDB(dbPath)
mux := http.NewServeMux()
// Pages
mux.HandleFunc("/", handleIndex)
mux.HandleFunc("/login", handleLogin)
mux.HandleFunc("/verify", handleVerify)
mux.HandleFunc("/dashboard", handleDashboard)
mux.HandleFunc("/checkout", handleCheckout)
mux.HandleFunc("/regions", handleRegions)
mux.HandleFunc("/settings", handleSettings)
// API
mux.HandleFunc("/api/auth/email", apiAuthEmail)
mux.HandleFunc("/api/auth/verify", apiAuthVerify)
mux.HandleFunc("/api/auth/logout", apiAuthLogout)
mux.HandleFunc("/api/checkout", apiCheckout)
mux.HandleFunc("/api/vaults", apiVaults)
mux.HandleFunc("/api/vault/create", apiVaultCreate)
mux.HandleFunc("/api/vault/", apiVaultDelete) // /api/vault/{id}/delete
mux.HandleFunc("/api/account", apiAccount)
// Static assets
mux.HandleFunc("/account.css", serveStatic("account.css", "text/css"))
mux.HandleFunc("/favicon.svg", serveStatic("favicon.svg", "image/svg+xml"))
// Strip .html extensions
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, ".html") {
http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, ".html"), http.StatusMovedPermanently)
return
}
mux.ServeHTTP(w, r)
})
log.Printf("clavitor account · :%s", port)
if devMode {
log.Println(" dev mode: templates reload from disk")
}
log.Fatal(http.ListenAndServe(":"+port, handler))
}
func loadTemplates(page string) (*template.Template, error) {
if devMode {
base := filepath.Join("templates", "base.tmpl")
pg := filepath.Join("templates", page+".tmpl")
return template.ParseFiles(base, pg)
}
return template.ParseFS(embedded, "templates/base.tmpl", "templates/"+page+".tmpl")
}
func render(w http.ResponseWriter, page string, data PageData) {
data.Base = basePath
tmpl, err := loadTemplates(page)
if err != nil {
log.Printf("template error: %v", err)
http.Error(w, "internal error", 500)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
log.Printf("render error: %v", err)
}
}
func serveStatic(name, contentType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var data []byte
var err error
if devMode {
data, err = os.ReadFile(name)
} else {
data, err = fs.ReadFile(embedded, name)
}
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(data)
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
http.Redirect(w, r, basePath+"/login", http.StatusTemporaryRedirect)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
// Already logged in? Go to dashboard
if authEmail(r) != "" {
http.Redirect(w, r, basePath+"/dashboard", http.StatusTemporaryRedirect)
return
}
render(w, "login", PageData{
Page: "login",
Title: "Sign in — clavitor",
Desc: "Sign in to your clavitor account",
})
}
func handleVerify(w http.ResponseWriter, r *http.Request) {
render(w, "verify", PageData{
Page: "verify",
Title: "Enter code — clavitor",
Desc: "Verify your login code",
Data: r.URL.Query().Get("email"),
})
}
func handleDashboard(w http.ResponseWriter, r *http.Request) {
if authEmail(r) == "" {
http.Redirect(w, r, basePath+"/login", http.StatusTemporaryRedirect)
return
}
render(w, "dashboard", PageData{
Page: "dashboard",
Title: "Dashboard — clavitor",
Desc: "Manage your vaults",
ActiveNav: "dashboard",
})
}
func handleCheckout(w http.ResponseWriter, r *http.Request) {
render(w, "checkout", PageData{
Page: "checkout",
Title: "Get started — clavitor",
Desc: "Create your clavitor account",
})
}
func handleRegions(w http.ResponseWriter, r *http.Request) {
if authEmail(r) == "" {
http.Redirect(w, r, basePath+"/login", http.StatusTemporaryRedirect)
return
}
render(w, "regions", PageData{
Page: "regions",
Title: "Choose your region — clavitor",
Desc: "Pick where your vault lives",
ActiveNav: "dashboard",
})
}
func handleSettings(w http.ResponseWriter, r *http.Request) {
if authEmail(r) == "" {
http.Redirect(w, r, basePath+"/login", http.StatusTemporaryRedirect)
return
}
render(w, "settings", PageData{
Page: "settings",
Title: "Settings — clavitor",
Desc: "Account settings",
ActiveNav: "settings",
})
}

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
<meta name="description" content="{{.Desc}}">
<meta name="robots" content="noindex, nofollow">
<link rel="icon" href="{{.Base}}/favicon.svg" type="image/svg+xml">
<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=Inter:wght@400;500;550;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{.Base}}/account.css">
</head>
<body>
<div class="ambient"></div>
<div class="gridlines"></div>
<div class="page">
{{template "page" .}}
</div>
{{template "scripts" .}}
</body>
</html>

View File

@ -0,0 +1,107 @@
{{define "page"}}
<div class="checkout-wrapper">
<div class="checkout-card glass fade-in">
<span class="label accent">Get started</span>
<h1 style="margin-top:0.5rem">Your vault awaits</h1>
<div class="price-display">
<span class="price-amount">$12</span>
<span class="price-period">/year</span>
</div>
<div class="price-note">
30-day money-back guarantee. No questions asked.
</div>
<hr class="divider">
<ul class="feature-list">
<li>
<svg class="check" viewBox="0 0 16 16" width="16" height="16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>
Zero-knowledge encryption — we cannot read your vault
</li>
<li>
<svg class="check" viewBox="0 0 16 16" width="16" height="16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>
Field-level AI visibility — agents see what you allow
</li>
<li>
<svg class="check" viewBox="0 0 16 16" width="16" height="16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>
23 global regions — pick where your data lives
</li>
<li>
<svg class="check" viewBox="0 0 16 16" width="16" height="16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>
WebAuthn PRF — hardware key is the only key
</li>
<li>
<svg class="check" viewBox="0 0 16 16" width="16" height="16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>
FIPS 140-3 certified cryptography
</li>
<li>
<svg class="check" viewBox="0 0 16 16" width="16" height="16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>
MCP, CLI, browser extension, REST API
</li>
</ul>
<hr class="divider">
<form id="checkout-form" onsubmit="return startCheckout(event)">
<div class="field">
<label for="checkout-email">Email address</label>
<input type="email" id="checkout-email" class="input" placeholder="you@example.com" required autocomplete="email" autofocus>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg" id="checkout-btn">
Continue to payment
</button>
</form>
<div class="login-footer">
Already have an account? <a href="{{.Base}}/login">Sign in</a>
</div>
<div style="text-align:center;margin-top:1rem">
<span style="font-size:0.75rem;color:var(--text-secondary)">
Payments processed by Stripe. Cancel anytime.
</span>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
async function startCheckout(e) {
e.preventDefault();
const email = document.getElementById('checkout-email').value;
const btn = document.getElementById('checkout-btn');
btn.disabled = true;
btn.textContent = 'Redirecting to Stripe…';
try {
const resp = await fetch('{{.Base}}/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (!resp.ok) throw new Error('Could not start checkout');
const { url } = await resp.json();
window.location.href = url;
} catch (err) {
showToast(err.message, 'error');
btn.disabled = false;
btn.textContent = 'Continue to payment';
}
}
function showToast(msg, type) {
let t = document.querySelector('.toast');
if (!t) {
t = document.createElement('div');
t.className = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.className = 'toast ' + type;
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => t.classList.remove('show'), type === 'error' ? 6000 : 3000);
}
</script>
{{end}}

View File

@ -0,0 +1,165 @@
{{define "page"}}
<header class="topbar">
<div class="container">
<div class="topbar-inner">
<a href="{{.Base}}/dashboard" class="topbar-brand">clavitor <span class="port">account</span></a>
<nav class="topbar-nav">
<a href="{{.Base}}/dashboard" class="{{if eq .ActiveNav "dashboard"}}active{{end}}">Vaults</a>
<a href="{{.Base}}/settings" class="{{if eq .ActiveNav "settings"}}active{{end}}">Settings</a>
<div class="separator"></div>
<a href="#" id="logout-link">Sign out</a>
</nav>
</div>
</div>
</header>
<main class="container" style="flex:1">
<section class="section fade-in">
<div class="section-header">
<div>
<span class="label accent">Account</span>
<h1 style="margin-top:0.5rem">Your vaults</h1>
</div>
<a href="{{.Base}}/regions" class="btn btn-primary" id="new-vault-btn">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New vault
</a>
</div>
</section>
<!-- Vault list (loaded dynamically) -->
<section class="fade-in fade-in-delay-1">
<div class="card" id="vault-list" style="padding:0; overflow:hidden">
<div class="vault-row" style="justify-content:center;color:var(--text-tertiary);padding:2rem">
Loading…
</div>
</div>
</section>
<!-- Plan info -->
<section class="section fade-in fade-in-delay-2">
<div class="card" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap">
<div>
<span class="label" style="margin-bottom:0.25rem;display:block">Plan</span>
<div style="font-weight:600;font-size:1.0625rem">Consumer</div>
<div class="text-secondary" style="font-size:0.8125rem;margin-top:0.125rem" id="plan-info">$12/year</div>
</div>
<a href="{{.Base}}/settings" class="btn btn-ghost">Manage billing</a>
</div>
</section>
<!-- Quick stats -->
<section class="fade-in fade-in-delay-3" style="padding-bottom:3rem">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem">
<div class="card" style="text-align:center;padding:1.25rem">
<div style="font-size:1.75rem;font-weight:700;letter-spacing:-0.03em" id="stat-vaults">-</div>
<div class="label" style="margin-top:0.375rem">Vaults</div>
</div>
<div class="card" style="text-align:center;padding:1.25rem">
<div style="font-size:1.75rem;font-weight:700;letter-spacing:-0.03em;color:var(--accent)" id="stat-days">-</div>
<div class="label" style="margin-top:0.375rem">Days left</div>
</div>
<div class="card" style="text-align:center;padding:1.25rem">
<div style="font-size:1.75rem;font-weight:700;letter-spacing:-0.03em" id="stat-regions">-</div>
<div class="label" style="margin-top:0.375rem">Regions</div>
</div>
</div>
</section>
</main>
{{end}}
{{define "scripts"}}
<script>
const BASE = '{{.Base}}';
const REGIONS = {
zurich:'Zürich, Switzerland', virginia:'Virginia, US', sanfrancisco:'San Francisco, US',
montreal:'Montréal, Canada', mexico:'Mexico City, Mexico', bogota:'Bogotá, Colombia',
saopaulo:'São Paulo, Brazil', santiago:'Santiago, Chile', buenosaires:'Buenos Aires, Argentina',
london:'London, UK', madrid:'Madrid, Spain',
istanbul:'Istanbul, Turkey', dubai:'Dubai, UAE', capetown:'Cape Town, South Africa',
mumbai:'Mumbai, India', singapore:'Singapore', hongkong:'Hong Kong',
seoul:'Seoul, South Korea', tokyo:'Tokyo, Japan', sydney:'Sydney, Australia',
almaty:'Almaty, Kazakhstan'
};
async function loadDashboard() {
try {
const resp = await fetch(BASE + '/api/vaults');
if (resp.status === 401) { window.location.href = BASE + '/login'; return; }
if (!resp.ok) throw new Error('Failed to load');
const data = await resp.json();
const list = document.getElementById('vault-list');
const vaults = data.vaults || [];
if (vaults.length === 0) {
list.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;padding:3rem 2rem;gap:1rem;text-align:center">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<h2 style="color:var(--text)">You have no vaults yet</h2>
<p style="color:var(--text-secondary);max-width:320px;font-size:0.9375rem;line-height:1.6">
Pick a region and create your first vault. Your data stays in that region — encrypted, zero-knowledge, yours.
</p>
<a href="${BASE}/regions" class="btn btn-primary btn-lg" style="margin-top:0.5rem">
Choose a region and create your vault
</a>
</div>`;
} else {
list.innerHTML = vaults.map(v => {
const expires = new Date(v.expires_at);
const days = Math.ceil((expires - new Date()) / 86400000);
const isHQ = v.region === 'zurich';
const status = days > 0 ? 'live' : 'expired';
const regionName = REGIONS[v.region] || v.region;
return `
<div class="vault-row">
<div style="display:flex;align-items:center;gap:0.75rem">
<span class="status-dot ${status}"></span>
<div>
<div class="vault-id">clavitor-${v.vault_id}</div>
<div class="vault-region">${regionName}</div>
</div>
</div>
<div class="vault-meta">
${isHQ ? '<span class="label gold">HQ</span>' : ''}
<span>${days > 0 ? 'Expires' : 'Expired'} ${expires.toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'})}</span>
</div>
</div>`;
}).join('');
}
// Stats
document.getElementById('stat-vaults').textContent = vaults.length;
const regions = new Set(vaults.map(v => v.region));
document.getElementById('stat-regions').textContent = regions.size;
if (vaults.length > 0) {
const earliest = vaults.map(v => new Date(v.expires_at)).sort((a,b) => a-b)[0];
const days = Math.max(0, Math.ceil((earliest - new Date()) / 86400000));
document.getElementById('stat-days').textContent = days;
} else {
document.getElementById('stat-days').textContent = '-';
}
// Hide "New vault" if at max
if (data.count >= data.max) {
document.getElementById('new-vault-btn').style.display = 'none';
}
} catch (err) {
document.getElementById('vault-list').innerHTML =
'<div class="vault-row" style="justify-content:center;color:var(--red)">Failed to load vaults</div>';
}
}
document.getElementById('logout-link')?.addEventListener('click', async (e) => {
e.preventDefault();
await fetch(BASE + '/api/auth/logout', { method: 'POST' });
window.location.href = BASE + '/login';
});
loadDashboard();
</script>
{{end}}

View File

@ -0,0 +1,99 @@
{{define "page"}}
<div class="login-wrapper">
<div class="login-card glass fade-in">
<div class="login-header">
<div class="brand">clavitor</div>
<p>Sign in to manage your vaults</p>
</div>
<button class="btn btn-passkey btn-block btn-lg" onclick="passkeyLogin()">
<svg class="passkey-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z"/>
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>
</svg>
Sign in with passkey
</button>
<div class="login-divider">or</div>
<form id="email-form" onsubmit="return emailLogin(event)">
<div class="field">
<label for="email">Email address</label>
<input type="email" id="email" class="input" placeholder="you@example.com" required autocomplete="email" autofocus>
</div>
<button type="submit" class="btn btn-ghost btn-block">
Continue with email
</button>
</form>
<div class="login-footer">
No account? <a href="{{.Base}}/checkout">Get started</a>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
async function passkeyLogin() {
const btn = document.querySelector('.btn-passkey');
const orig = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span style="opacity:0.6">Waiting for authenticator…</span>';
try {
// WebAuthn authentication
const resp = await fetch('{{.Base}}/api/auth/passkey/options', { method: 'POST' });
if (!resp.ok) throw new Error('Server error');
const options = await resp.json();
// TODO: navigator.credentials.get() with options
// For now, simulate
await new Promise(r => setTimeout(r, 800));
// On success, redirect to dashboard
window.location.href = '{{.Base}}/dashboard';
} catch (err) {
showToast(err.message || 'Authentication failed', 'error');
btn.disabled = false;
btn.innerHTML = orig;
}
}
async function emailLogin(e) {
e.preventDefault();
const email = document.getElementById('email').value;
const btn = e.target.querySelector('button');
btn.disabled = true;
btn.textContent = 'Sending link…';
try {
const resp = await fetch('{{.Base}}/api/auth/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (!resp.ok) throw new Error('Could not send login code');
// Redirect to code entry page
window.location.href = '{{.Base}}/verify?email=' + encodeURIComponent(email);
} catch (err) {
showToast(err.message, 'error');
btn.disabled = false;
btn.textContent = 'Continue with email';
}
}
function showToast(msg, type) {
let t = document.querySelector('.toast');
if (!t) {
t = document.createElement('div');
t.className = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.className = 'toast ' + type;
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => t.classList.remove('show'), type === 'error' ? 6000 : 3000);
}
</script>
{{end}}

View File

@ -0,0 +1,263 @@
{{define "page"}}
<header class="topbar">
<div class="container">
<div class="topbar-inner">
<a href="{{.Base}}/dashboard" class="topbar-brand">clavitor <span class="port">account</span></a>
<nav class="topbar-nav">
<a href="{{.Base}}/dashboard">Vaults</a>
<a href="{{.Base}}/settings">Settings</a>
</nav>
</div>
</div>
</header>
<main class="container" style="flex:1">
<section class="section fade-in">
<span class="label accent">New vault</span>
<h1 style="margin-top:0.5rem">Choose your region</h1>
<p class="text-secondary" style="margin-top:0.5rem;max-width:480px">
Your vault lives in one region. All data stays there. Pick the closest location for the best performance.
</p>
</section>
<section class="fade-in fade-in-delay-1" style="padding-bottom:1.5rem">
<div class="label gold" style="margin-bottom:0.75rem">Headquarters</div>
<div class="region-grid">
<div class="card card-interactive card-gold region-card" data-region="zurich" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E8;&#x1F1ED;</span>
<span class="region-name">Zürich</span>
<span class="region-location">Switzerland</span>
<span class="label gold">HQ &middot; Swiss jurisdiction</span>
</div>
</div>
</section>
<section class="fade-in fade-in-delay-2" style="padding-bottom:1.5rem">
<div class="label" style="margin-bottom:0.75rem">Americas</div>
<div class="region-grid">
<div class="card card-interactive region-card" data-region="virginia" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1FA;&#x1F1F8;</span>
<span class="region-name">Virginia</span>
<span class="region-location">US East</span>
</div>
<div class="card card-interactive region-card" data-region="sanfrancisco" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1FA;&#x1F1F8;</span>
<span class="region-name">San Francisco</span>
<span class="region-location">US West</span>
</div>
<div class="card card-interactive region-card" data-region="montreal" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E8;&#x1F1E6;</span>
<span class="region-name">Montréal</span>
<span class="region-location">Canada</span>
</div>
<div class="card card-interactive region-card" data-region="mexico" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1F2;&#x1F1FD;</span>
<span class="region-name">Mexico City</span>
<span class="region-location">Mexico</span>
</div>
<div class="card card-interactive region-card" data-region="bogota" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E8;&#x1F1F4;</span>
<span class="region-name">Bogotá</span>
<span class="region-location">Colombia</span>
</div>
<div class="card card-interactive region-card" data-region="saopaulo" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E7;&#x1F1F7;</span>
<span class="region-name">São Paulo</span>
<span class="region-location">Brazil</span>
</div>
<div class="card card-interactive region-card" data-region="santiago" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E8;&#x1F1F1;</span>
<span class="region-name">Santiago</span>
<span class="region-location">Chile</span>
</div>
<div class="card card-interactive region-card" data-region="buenosaires" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E6;&#x1F1F7;</span>
<span class="region-name">Buenos Aires</span>
<span class="region-location">Argentina</span>
</div>
</div>
</section>
<section class="fade-in fade-in-delay-3" style="padding-bottom:1.5rem">
<div class="label" style="margin-bottom:0.75rem">Europe</div>
<div class="region-grid">
<div class="card card-interactive region-card" data-region="london" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1EC;&#x1F1E7;</span>
<span class="region-name">London</span>
<span class="region-location">United Kingdom</span>
</div>
<div class="card card-interactive region-card" data-region="madrid" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1EA;&#x1F1F8;</span>
<span class="region-name">Madrid</span>
<span class="region-location">Spain</span>
</div>
</div>
</section>
<section class="fade-in fade-in-delay-3" style="padding-bottom:1.5rem">
<div class="label" style="margin-bottom:0.75rem">Middle East &amp; Africa</div>
<div class="region-grid">
<div class="card card-interactive region-card" data-region="istanbul" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1F9;&#x1F1F7;</span>
<span class="region-name">Istanbul</span>
<span class="region-location">Turkey</span>
</div>
<div class="card card-interactive region-card" data-region="dubai" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E6;&#x1F1EA;</span>
<span class="region-name">Dubai</span>
<span class="region-location">UAE</span>
</div>
<div class="card card-interactive region-card" data-region="capetown" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1FF;&#x1F1E6;</span>
<span class="region-name">Cape Town</span>
<span class="region-location">South Africa</span>
</div>
</div>
</section>
<section class="fade-in fade-in-delay-3" style="padding-bottom:3rem">
<div class="label" style="margin-bottom:0.75rem">Asia Pacific</div>
<div class="region-grid">
<div class="card card-interactive region-card" data-region="mumbai" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1EE;&#x1F1F3;</span>
<span class="region-name">Mumbai</span>
<span class="region-location">India</span>
</div>
<div class="card card-interactive region-card" data-region="singapore" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1F8;&#x1F1EC;</span>
<span class="region-name">Singapore</span>
<span class="region-location">Singapore</span>
</div>
<div class="card card-interactive region-card" data-region="hongkong" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1ED;&#x1F1F0;</span>
<span class="region-name">Hong Kong</span>
<span class="region-location">China</span>
</div>
<div class="card card-interactive region-card" data-region="seoul" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1F0;&#x1F1F7;</span>
<span class="region-name">Seoul</span>
<span class="region-location">South Korea</span>
</div>
<div class="card card-interactive region-card" data-region="tokyo" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1EF;&#x1F1F5;</span>
<span class="region-name">Tokyo</span>
<span class="region-location">Japan</span>
</div>
<div class="card card-interactive region-card" data-region="sydney" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1E6;&#x1F1FA;</span>
<span class="region-name">Sydney</span>
<span class="region-location">Australia</span>
</div>
<div class="card card-interactive region-card" data-region="almaty" onclick="selectRegion(this)">
<span class="region-flag">&#x1F1F0;&#x1F1FF;</span>
<span class="region-name">Almaty</span>
<span class="region-location">Kazakhstan</span>
</div>
</div>
</section>
<!-- Sticky bottom bar -->
<div id="region-confirm" style="position:fixed;bottom:0;left:0;right:0;transform:translateY(100%);transition:transform 0.35s cubic-bezier(0.16,1,0.3,1);z-index:50">
<div style="background:rgba(6,10,16,0.9);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-top:1px solid var(--glass-border);padding:1rem 0">
<div class="container" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;max-width:960px">
<div>
<span id="selected-name" style="font-weight:600"></span>
<span id="selected-location" class="text-secondary" style="font-size:0.875rem;margin-left:0.5rem"></span>
</div>
<button class="btn btn-primary" onclick="confirmRegion()">
Create vault here
</button>
</div>
</div>
</div>
</main>
{{end}}
{{define "scripts"}}
<script>
let selectedRegion = null;
function selectRegion(el) {
document.querySelectorAll('.region-card.selected').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
selectedRegion = el.dataset.region;
const name = el.querySelector('.region-name').textContent;
const loc = el.querySelector('.region-location').textContent;
document.getElementById('selected-name').textContent = name;
document.getElementById('selected-location').textContent = loc;
document.getElementById('region-confirm').style.transform = 'translateY(0)';
}
async function confirmRegion() {
if (!selectedRegion) return;
const btn = document.querySelector('#region-confirm .btn');
btn.disabled = true;
btn.textContent = 'Creating vault…';
try {
const resp = await fetch('{{.Base}}/api/vault/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ region: selectedRegion })
});
if (resp.status === 401) { window.location.href = '{{.Base}}/login'; return; }
if (resp.status === 409) throw new Error('Vault limit reached');
if (!resp.ok) throw new Error('Could not create vault');
window.location.href = '{{.Base}}/dashboard';
} catch (err) {
showToast(err.message || 'Could not create vault', 'error');
btn.disabled = false;
btn.textContent = 'Create vault here';
}
}
function showToast(msg, type) {
let t = document.querySelector('.toast');
if (!t) {
t = document.createElement('div');
t.className = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.className = 'toast ' + type;
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => t.classList.remove('show'), type === 'error' ? 6000 : 3000);
}
// Auto-detect closest region
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
const regions = {
zurich: [47.37, 8.54], virginia: [38.95, -77.45], sanfrancisco: [37.77, -122.42],
montreal: [45.50, -73.57], mexico: [19.43, -99.13], bogota: [4.71, -74.07],
saopaulo: [-23.55, -46.63], santiago: [-33.45, -70.65], buenosaires: [-34.60, -58.38],
london: [51.51, -0.13], madrid: [40.42, -3.70],
istanbul: [41.01, 28.98], dubai: [25.20, 55.27], capetown: [-33.92, 18.42],
mumbai: [19.08, 72.88], singapore: [1.35, 103.82], hongkong: [22.32, 114.17],
seoul: [37.57, 126.98], tokyo: [35.68, 139.69], sydney: [-33.87, 151.21],
almaty: [43.24, 76.95]
};
let closest = null, minDist = Infinity;
for (const [id, [rlat, rlon]] of Object.entries(regions)) {
const d = Math.hypot(lat - rlat, lon - rlon);
if (d < minDist) { minDist = d; closest = id; }
}
if (closest) {
const el = document.querySelector(`[data-region="${closest}"]`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.style.boxShadow = '0 0 0 2px var(--accent-dim)';
const hint = document.createElement('span');
hint.className = 'label accent';
hint.textContent = 'Nearest to you';
hint.style.marginTop = '0.25rem';
el.appendChild(hint);
}
}
}, () => {}, { timeout: 5000 });
}
</script>
{{end}}

View File

@ -0,0 +1,179 @@
{{define "page"}}
<header class="topbar">
<div class="container">
<div class="topbar-inner">
<a href="{{.Base}}/dashboard" class="topbar-brand">clavitor <span class="port">account</span></a>
<nav class="topbar-nav">
<a href="{{.Base}}/dashboard" class="{{if eq .ActiveNav "dashboard"}}active{{end}}">Vaults</a>
<a href="{{.Base}}/settings" class="{{if eq .ActiveNav "settings"}}active{{end}}">Settings</a>
<div class="separator"></div>
<a href="{{.Base}}/login" id="logout-link">Sign out</a>
</nav>
</div>
</div>
</header>
<main class="container" style="flex:1">
<section class="section fade-in">
<span class="label">Account</span>
<h1 style="margin-top:0.5rem">Settings</h1>
</section>
<section class="card fade-in fade-in-delay-1">
<!-- Email -->
<div class="settings-section">
<div class="settings-row">
<div>
<div class="settings-label">Email</div>
<div class="settings-desc">johan@example.com</div>
</div>
</div>
</div>
<!-- Authentication -->
<div class="settings-section">
<div class="settings-row">
<div>
<div class="settings-label">Authentication</div>
<div class="settings-desc">
Passkey registered &middot;
<span class="text-accent" style="font-size:0.8125rem">Active</span>
</div>
</div>
<button class="btn btn-ghost" onclick="registerPasskey()">Add passkey</button>
</div>
</div>
<!-- Billing -->
<div class="settings-section">
<div class="settings-row">
<div>
<div class="settings-label">Billing</div>
<div class="settings-desc">
$12/year &middot; Next charge Mar 20, 2027
</div>
</div>
<button class="btn btn-ghost" onclick="manageBilling()">Manage in Stripe</button>
</div>
</div>
<!-- Invoices -->
<div class="settings-section">
<div class="settings-row">
<div>
<div class="settings-label">Invoices</div>
<div class="settings-desc">View and download past invoices</div>
</div>
<button class="btn btn-ghost" onclick="viewInvoices()">View invoices</button>
</div>
</div>
</section>
<!-- Danger zone -->
<section class="section fade-in fade-in-delay-2" style="padding-bottom:3rem">
<div class="danger-zone">
<h3>Danger zone</h3>
<p>
Deleting your account cancels your subscription, deletes all vaults,
and removes all data. This cannot be undone.
</p>
<button class="btn btn-danger" onclick="deleteAccount()">
Delete account
</button>
</div>
</section>
</main>
{{end}}
{{define "scripts"}}
<script>
function registerPasskey() {
// TODO: WebAuthn registration flow for account site
showToast('Passkey registration not yet implemented', 'error');
}
async function manageBilling() {
try {
const resp = await fetch('{{.Base}}/api/billing/portal', { method: 'POST' });
if (!resp.ok) throw new Error('Could not open billing portal');
const { url } = await resp.json();
window.location.href = url;
} catch (err) {
showToast(err.message, 'error');
}
}
function viewInvoices() {
manageBilling(); // Stripe portal handles invoices too
}
function deleteAccount() {
// Two-step confirmation
const zone = document.querySelector('.danger-zone');
if (zone.dataset.confirming) {
performDelete();
return;
}
zone.dataset.confirming = 'true';
const btn = zone.querySelector('.btn-danger');
btn.textContent = 'Confirm — delete everything';
btn.style.background = 'rgba(239,68,68,0.25)';
// Add email confirmation input
const field = document.createElement('div');
field.className = 'field';
field.style.marginBottom = '1rem';
field.innerHTML = `
<label style="color:var(--red)">Type your email to confirm</label>
<input type="email" id="delete-confirm-email" class="input" placeholder="you@example.com" style="border-color:rgba(239,68,68,0.3)">
`;
zone.insertBefore(field, btn);
document.getElementById('delete-confirm-email').focus();
}
async function performDelete() {
const email = document.getElementById('delete-confirm-email')?.value;
if (!email) {
showToast('Please enter your email to confirm', 'error');
return;
}
const btn = document.querySelector('.danger-zone .btn-danger');
btn.disabled = true;
btn.textContent = 'Deleting…';
try {
const resp = await fetch('{{.Base}}/api/account/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
if (!resp.ok) throw new Error('Could not delete account');
window.location.href = '/login?deleted=1';
} catch (err) {
showToast(err.message, 'error');
btn.disabled = false;
btn.textContent = 'Confirm — delete everything';
}
}
document.getElementById('logout-link')?.addEventListener('click', (e) => {
e.preventDefault();
window.location.href = '{{.Base}}/login';
});
function showToast(msg, type) {
let t = document.querySelector('.toast');
if (!t) {
t = document.createElement('div');
t.className = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.className = 'toast ' + type;
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => t.classList.remove('show'), type === 'error' ? 6000 : 3000);
}
</script>
{{end}}

View File

@ -0,0 +1,73 @@
{{define "page"}}
<div class="login-wrapper">
<div class="login-card glass fade-in">
<div class="login-header">
<div class="brand">clavitor</div>
<p>Enter the code sent to your email</p>
</div>
<form id="verify-form" onsubmit="return verifyCode(event)">
<input type="hidden" id="verify-email" value="{{.Data}}">
<div class="field">
<label for="code">Verification code</label>
<input type="text" id="code" class="input input-mono" placeholder="123456"
required autocomplete="one-time-code" autofocus
maxlength="6" pattern="[0-9]{6}" inputmode="numeric"
style="text-align:center;font-size:1.5rem;letter-spacing:0.3em">
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg" id="verify-btn">
Verify
</button>
</form>
<div class="login-footer">
<a href="{{.Base}}/login">Back to sign in</a>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
async function verifyCode(e) {
e.preventDefault();
const email = document.getElementById('verify-email').value;
const code = document.getElementById('code').value;
const btn = document.getElementById('verify-btn');
btn.disabled = true;
btn.textContent = 'Verifying…';
try {
const resp = await fetch('{{.Base}}/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, code })
});
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || 'Invalid code');
}
window.location.href = '{{.Base}}/dashboard';
} catch (err) {
showToast(err.message, 'error');
btn.disabled = false;
btn.textContent = 'Verify';
document.getElementById('code').value = '';
document.getElementById('code').focus();
}
}
function showToast(msg, type) {
let t = document.querySelector('.toast');
if (!t) {
t = document.createElement('div');
t.className = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.className = 'toast ' + type;
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => t.classList.remove('show'), type === 'error' ? 6000 : 3000);
}
</script>
{{end}}

Binary file not shown.

BIN
commercial/marketing/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

View File

@ -0,0 +1,110 @@
# clavitor — POP Map (AWS + is*hosting)
## Provider Strategy
- **AWS** — core markets where they have regions
- **is*hosting** — everything else (real infrastructure, verified via Looking Glass, Equinix/Tier-3+ DCs)
- Per-record replication for egress efficiency
- Single Go binary + SQLite — no shared state, fully horizontal
---
## The Map (23 confirmed POPs)
### Americas (8)
| City | Provider | DC | Notes |
|------|----------|----|-------|
| Virginia 🇺🇸 | AWS us-east-1 | AWS | US East |
| San Francisco 🇺🇸 | AWS us-west-1 | AWS | US West |
| Montréal 🇨🇦 | is*hosting | eStruxture | Canada |
| Mexico City 🇲🇽 | is*hosting | CloudHQ QRO1 (Querétaro) | LatAm North |
| Bogotá 🇨🇴 | is*hosting | EdgeUno BOG6 (Cota) | LatAm Mid |
| São Paulo 🇧🇷 | AWS sa-east-1 / is*hosting | Equinix SP4 | LatAm largest |
| Santiago 🇨🇱 | is*hosting | EdgeUno SCL2 | LatAm South |
| Buenos Aires 🇦🇷 | is*hosting | EdgeUno EZE1 | LatAm South alt |
### Europe (3)
| City | Provider | DC | Notes |
|------|----------|----|-------|
| London 🇬🇧 | AWS eu-west-2 | AWS | UK + Western Europe |
| Zürich 🇨🇭 | is*hosting | Digitale Suisse AG | Central Europe |
| Madrid 🇪🇸 | is*hosting | Interxion | Southern Europe |
### Middle East & Turkey (2)
| City | Provider | DC | Notes |
|------|----------|----|-------|
| Istanbul 🇹🇷 | is*hosting | Premier DC | Bridge EU/ME |
| Dubai 🇦🇪 | is*hosting | Equinix DX1 | Gulf hub |
### Africa (1)
| City | Provider | DC | Notes |
|------|----------|----|-------|
| Cape Town 🇿🇦 | AWS af-south-1 | AWS | South Africa — only confirmed AWS region in Africa |
### Asia-Pacific (9)
| City | Provider | DC | Notes |
|------|----------|----|-------|
| Mumbai 🇮🇳 | AWS ap-south-1 | AWS | South Asia |
| Singapore 🇸🇬 | is*hosting / AWS | Equinix | SE Asia hub |
| Hong Kong 🇭🇰 | is*hosting | Equinix | China gateway |
| Seoul 🇰🇷 | AWS ap-northeast-2 | AWS | Korea |
| Tokyo 🇯🇵 | is*hosting | Equinix TY8 | Japan |
| Sydney 🇦🇺 | is*hosting | Equinix SY2 | Australia |
| Almaty 🇰🇿 | is*hosting | Sairam | Central Asia — unique, verified |
| Lima 🇵🇪 | is*hosting | GTD Surco | Peru — bonus LatAm coverage |
| ~~Auckland 🇳🇿~~ | ❌ | — | **BLACK SPOT** — no AWS/is*hosting in NZ, nearest node Sydney |
---
## ⚠️ Black Spots
### West Africa
- **Lagos 🇳🇬** — Azure hub only, not a deployable region. **Black spot.**
- **Accra 🇬🇭, Dakar 🇸🇳, Abidjan 🇨🇮** — nothing real available
- All of West Africa = black spot. Starlink + local NUC is the only honest option currently.
### East Africa
- **Nairobi 🇰🇪** — Azure hub only, not a deployable region. **Black spot.**
- **Dar es Salaam 🇹🇿, Kampala 🇺🇬, Addis Ababa 🇪🇹** — not in is*hosting, no AWS
- All of East Africa = black spot. Same situation as West Africa.
### TurkeyEgypt region
- **Istanbul ✅** — is*hosting, Premier DC
- **Cairo 🇪🇬** — NOT in is*hosting's 41 locations. AWS has no Egypt region yet. Azure expanding but not GA.
- Cairo = **black spot**. Closest real nodes are Istanbul (~1800km) or Dubai (~2500km).
### New Zealand 🇳🇿
- **Auckland** — not in is*hosting. AWS has no NZ region. Azure has no NZ region.
- Nearest real node: Sydney (~2100km).
- Auckland = **black spot**. Would need a local provider (Spark NZ, Vodafone NZ colocation) or Starlink.
### Russia 🇷🇺
- **Moscow** — is*hosting has no Russia node (sanctions exposure, payment issues)
- AWS, Azure, GCP all withdrew from Russia in 2022
- Russia = **black spot by design** — no safe, commercially viable option exists
- Kyiv is in is*hosting but that's Ukraine, not a Russia workaround
### China 🇨🇳
- **Hong Kong ✅** — as close as you get without operating inside China
- **Mainland China** — requires ICP license from MIIT, mandatory backdoor access, legally incompatible with clavitor's operator-blind model
- China mainland = **black spot by architecture** — cannot be done legally without compromising the product
---
## Summary
| Region | POPs | Black spots |
|--------|------|-------------|
| Americas | 8 | None |
| Europe | 3 | None |
| Middle East/Turkey | 2 | Cairo |
| Africa | 1 | All of West Africa, all of East Africa, Cairo |
| Asia-Pacific | 9 | Auckland, Russia, China mainland |
| **Total** | **25** | |
---
## Notes
- Lima added from is*hosting (was previously missing from LatAm)
- Atal Networks removed — SEO-only, no real exotic infrastructure
- Russia and China mainland are architectural black spots, not operational failures
- Auckland and Cairo are genuine gaps — local provider research needed

View File

@ -0,0 +1,237 @@
# clavitor — Twitter Follow List
*Target: @clavitor. Follow these to build visibility, get indexed by the algorithm, and appear in relevant feeds.*
---
## 1. OpenClaw + AI Agent Ecosystem
| Handle | Name | Why |
|--------|------|-----|
| @OpenClaw | OpenClaw | Core partner — our agent platform |
| @AnthropicAI | Anthropic | Claude's creator — agent infrastructure |
| @OpenAI | OpenAI | GPT / operator — largest agent ecosystem |
| @GoogleDeepMind | Google DeepMind | Gemini — key agent provider |
| @MistralAI | Mistral AI | Open-weight models — agent use cases |
| @CohereAI | Cohere | Enterprise agent platform |
| @LangChainAI | LangChain | Most-used agent framework |
| @LlamaIndex | LlamaIndex | Agent orchestration framework |
| @CrewAIInc | CrewAI | Multi-agent framework — growing fast |
| @AutoGenAI | AutoGen | Microsoft's agent framework |
| @MCPProtocol | MCP Protocol | Model Context Protocol — our integration layer |
| @composio_dev | Composio | Agent tool integrations |
| @e2bdev | E2B | Code execution for agents |
| @BrowserUse | Browser Use | Web agents |
| @PlaywrightTest | Playwright | Browser automation |
| @OpenInterpreter | Open Interpreter | Local agent runtime |
| @HuggingFace | Hugging Face | Model hub — agent community |
| @togethercompute | Together AI | Agent inference platform |
| @GroqInc | Groq | Fast inference for agents |
| @perplexity_ai | Perplexity | AI search — agent adjacent |
---
## 2. AI Agent Influencers — "The Promotion Machine"
*These accounts have large audiences and regularly promote new AI projects. Getting a mention = distribution.*
| Handle | Name | Why |
|--------|------|-----|
| @AlexReynolds_AI | Alex Finn / AI promoters | Large following, actively promotes AI launches |
| @aiDotEngineer | AI Engineer | Dev-focused AI content, large reach |
| @swyx | swyx (Shawn Wang) | AI dev community builder — latent.space |
| @karpathy | Andrej Karpathy | Former OpenAI/Tesla — millions of followers |
| @ylecun | Yann LeCun | Meta's Chief AI Scientist — authority signal |
| @GaryMarcus | Gary Marcus | AI critic — gets engagement from controversy |
| @emollick | Ethan Mollick | Wharton professor — viral AI use cases |
| @sama | Sam Altman | OpenAI CEO — follow for algo signal |
| @demishassabis | Demis Hassabis | DeepMind CEO |
| @ylecun | Yann LeCun | Meta AI Chief |
| @jeffdean | Jeff Dean | Google AI |
| @hardmaru | David Ha | Research director |
| @fchollet | François Chollet | Keras creator |
| @mmitchell_ai | Margaret Mitchell | AI ethics — engaged audience |
| @abhi1nandy2 | Abhishek Thakur | Kaggle grandmaster, huge ML following |
| @_akhaliq | AK | Papers + models — daily AI news aggregator |
| @rohanpaul_ai | Rohan Paul | AI content, large audience |
| @aibreakfast | AI Breakfast | Daily AI newsletter/X presence |
| @TheMoonCarlson | Carl | AI tools promotion account |
| @kimmonismus | Kim Monismus | AI tools reviewer, German-speaking audience |
| @javilopen | Javi Lopez | Viral AI demos |
| @levelsio | Pieter Levels | Indie hacker + AI builder — huge reach |
| @yoheinakajima | Yohei Nakajima | BabyAGI creator — agent community OG |
| @SigGravitas | Auto-GPT | AutoGPT creator |
| @rikarends | Rik Arends | Makepad / dev tools builder |
| @danshipper | Dan Shipper | Every.to — AI writing/tools community |
---
## 3. All Major AI Labs & Studios
### US Labs
| Handle | Name | Why |
|--------|------|-----|
| @AnthropicAI | Anthropic | Claude |
| @OpenAI | OpenAI | GPT |
| @Google_AI | Google AI | Gemini |
| @MetaAI | Meta AI | Llama |
| @MicrosoftAI | Microsoft AI | Copilot ecosystem |
| @AmazonAWS | Amazon AWS | Bedrock |
| @NVIDIAAIDev | NVIDIA AI | GPU / inference infrastructure |
| @MistralAI | Mistral | Open models |
| @AI21Labs | AI21 Labs | Jamba |
| @CohereAI | Cohere | Enterprise NLP |
| @adept_ai | Adept | Action-focused agents |
| @CharacterAI | Character.AI | Consumer AI |
| @InflectionAI | Inflection | Pi AI |
| @xAI | xAI | Grok |
| @Scale_AI | Scale AI | Data + evaluation |
| @HumanLoopHQ | HumanLoop | LLM ops |
### Chinese AI Studios
| Handle | Name | Why |
|--------|------|-----|
| @BaiduAI | Baidu AI | ERNIE — huge Chinese user base |
| @AlibabaCloud | Alibaba Cloud | Qwen models |
| @ByteDanceTech | ByteDance | Doubao — massive distribution |
| @DeepSeek_AI | DeepSeek | R1 — viral open-source model |
| @moonshot_ai | Moonshot AI | Kimi — Chinese long-context model |
| @Zhipu_AI | Zhipu AI | GLM series |
| @01ai_Yi | 01.AI | Yi models — Kai-Fu Lee |
| @stepfun_ai | StepFun | Multimodal Chinese models |
| @MiniMaxAI | MiniMax | Chinese consumer AI |
### European AI Studios
| Handle | Name | Why |
|--------|------|-----|
| @AlephAlpha | Aleph Alpha | German sovereign AI |
| @Mistral_AI | Mistral | French open models |
| @CriticalFuture_ | Critical Future | UK AI safety |
---
## 4. Inference & API Platforms
| Handle | Name | Why |
|--------|------|-----|
| @FireworksAI | Fireworks AI | Fast inference API — agent infrastructure |
| @togethercompute | Together AI | Multi-model inference |
| @GroqInc | Groq | Fastest inference — agent use |
| @ReplicateHQ | Replicate | Model deployment |
| @modal_labs | Modal Labs | Serverless AI infra |
| @fly_io | Fly.io | Distributed compute |
| @CloudflareDev | Cloudflare Workers AI | Edge inference |
---
## 5. Password Managers & Security Tools
*Know your competition. Engage with their audience.*
| Handle | Name | Why |
|--------|------|-----|
| @1Password | 1Password | Biggest competitor |
| @Bitwarden | Bitwarden | Open-source competitor |
| @Dashlane | Dashlane | Consumer competitor |
| @LastPass | LastPass | Breach-damaged — our contrast story |
| @NordPass | NordPass | Growing competitor |
| @ProtonPrivacy | Proton | Privacy-first — adjacent audience |
| @VaultWarden | Vaultwarden | Self-hosted Bitwarden fork community |
| @KeePass | KeePass | Self-hosted community |
| @hashicorp | HashiCorp | Vault — enterprise secrets management |
| @CyberArkSoftware | CyberArk | Enterprise PAM |
| @BeyondTrust | BeyondTrust | Enterprise PAM |
---
## 6. Hacker News + Tech Community
| Handle | Name | Why |
|--------|------|-----|
| @newsycombinator | Hacker News | HN official — follow + engage |
| @paulg | Paul Graham | YC founder — HN creator |
| @garrytan | Garry Tan | YC CEO — startup amplification |
| @TechCrunch | TechCrunch | Coverage + audience |
| @TheHackerNews | The Hacker News | Security news — cybersecurity audience |
| @SecurityWeekly | Security Weekly | Infosec podcast/community |
| @KrebsOnSecurity | Brian Krebs | Breach journalism — our audience |
| @troyhunt | Troy Hunt | HaveIBeenPwned — breach audience |
| @thegrugq | thegrugq | Security researcher — respected voice |
| @SwiftOnSecurity | SwiftOnSecurity | Security culture — huge following |
| @LobstersCoop | Lobste.rs | Developer community |
| @devto | DEV Community | Developer platform |
| @hackernewsbot | HN Bot | Automated HN posts |
---
## 7. Cybersecurity & Privacy Experts
| Handle | Name | Why |
|--------|------|-----|
| @troyhunt | Troy Hunt | HIBP creator — breach community |
| @briankrebs | Brian Krebs | Investigative security journalist |
| @evacide | Eva Galperin | EFF — digital rights audience |
| @EFF | Electronic Frontier Foundation | Digital rights — privacy community |
| @privacytools | Privacy Tools | Privacy-focused community |
| @MITREattack | MITRE ATT&CK | Threat intelligence community |
| @NSACyber | NSA Cybersecurity | Government security |
| @CISAgov | CISA | US cyber agency |
| @sans_isc | SANS Internet Storm Center | Security community |
| @schneierblog | Bruce Schneier | Security legend |
| @taviso | Tavis Ormandy | Google Project Zero — elite researcher |
| @lcamtuf | lcamtuf | AFL creator, security researcher |
| @GrahamCluley | Graham Cluley | Security journalist + podcaster |
| @SophosNews | Sophos | Security vendor with engaged community |
| @MalwareHunterTeam | MHT | Malware research — infosec community |
---
## 8. Tech Giants (for algo signal + enterprise audience)
| Handle | Name | Why |
|--------|------|-----|
| @Apple | Apple | Consumer signal, privacy brand alignment |
| @Microsoft | Microsoft | Enterprise audience |
| @Google | Google | Broad tech audience |
| @awscloud | AWS | Our infrastructure — relevant audience |
| @GitHub | GitHub | Developer community |
| @vercel | Vercel | Developer platform — agent builders |
| @stripe | Stripe | Developer-focused — our audience |
| @hashicorp | HashiCorp | Infrastructure + secrets management |
---
## 9. Privacy & Data Regulation
| Handle | Name | Why |
|--------|------|-----|
| @EFF | EFF | Digital rights — privacy aligned |
| @PrivacyIntl | Privacy International | Global privacy advocacy |
| @EPDPAuthority | EDPB | EU data protection |
| @ICOnews | UK ICO | UK GDPR enforcement |
| @FTC | FTC | US regulator — breach news source |
| @ENISA_EU | ENISA | EU cybersecurity agency |
---
## Summary Stats
| Category | Count |
|----------|-------|
| OpenClaw + Agent Ecosystem | 20 |
| AI Influencers | 26 |
| AI Labs (US) | 16 |
| AI Labs (Chinese) | 9 |
| AI Labs (European) | 3 |
| Inference Platforms | 7 |
| Password Managers & Security Tools | 11 |
| HN + Tech Community | 13 |
| Cybersecurity Experts | 15 |
| Tech Giants | 8 |
| Privacy & Regulation | 6 |
| **Total** | **134** |
---
*Priority order: Agent ecosystem first (direct audience), AI influencers second (amplification), security/privacy third (relevance), rest for algo signal.*
*Generated by George · clavitor · March 2026*

View File

@ -0,0 +1,326 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>clavitor — Twitter Drip Schedule</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e0e0e0; padding: 40px 20px; }
h1 { font-size: 22px; font-weight: 700; color: #fff; margin-bottom: 6px; }
.subtitle { color: #666; font-size: 13px; margin-bottom: 40px; }
.week { margin-bottom: 48px; }
.week-label { font-size: 11px; text-transform: uppercase; letter-spacing: 2px; color: #555; margin-bottom: 16px; border-bottom: 1px solid #222; padding-bottom: 8px; }
.posts { display: flex; flex-direction: column; gap: 16px; }
.post { display: grid; grid-template-columns: 180px 1fr 280px; gap: 20px; background: #161616; border: 1px solid #222; border-radius: 10px; padding: 20px; align-items: start; }
.post.posted { border-left: 3px solid #1d9bf0; }
.post.scheduled { border-left: 3px solid #555; }
.post.pending { border-left: 3px solid #333; opacity: 0.7; }
.meta { display: flex; flex-direction: column; gap: 6px; }
.date { font-size: 15px; font-weight: 600; color: #fff; }
.time { font-size: 13px; color: #888; }
.lang-badge { display: inline-block; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; padding: 2px 8px; border-radius: 4px; margin-top: 4px; width: fit-content; }
.en { background: #1d3a5e; color: #7bb8f0; }
.zh { background: #3a1a1a; color: #f07b7b; }
.de { background: #1a2e1a; color: #7bf07b; }
.pt { background: #2e2a1a; color: #f0d07b; }
.status { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-top: 8px; }
.status.live { color: #1d9bf0; }
.status.queued { color: #aaa; }
.status.tbd { color: #555; }
.tweet-text { font-size: 14px; line-height: 1.6; color: #ccc; white-space: pre-wrap; }
.hashtags { color: #1d9bf0; }
.city-img { width: 100%; }
.city-img img { width: 100%; border-radius: 8px; display: block; aspect-ratio: 16/9; object-fit: cover; }
@media (max-width: 800px) { .post { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<h1>clavitor — Twitter Drip Schedule</h1>
<p class="subtitle">POP launch sequence · @clavitor · Every other day · AI-generated city images</p>
<!-- WEEK 1 -->
<div class="week">
<div class="week-label">Week 1 — March 1721, 2026</div>
<div class="posts">
<div class="post posted">
<div class="meta">
<div class="date">Tue, Mar 17</div>
<div class="time">6:43 PM EDT</div>
<span class="lang-badge en">EN</span>
<div class="status live">✓ Live</div>
</div>
<div class="tweet-text">Finally. A password manager where the company hosting your vault cannot read it.
clavitor is live in the United States 🇺🇸 — and your AI agents can use it without anyone else seeing what's inside.
Free till May 1st.
<span class="hashtags">#privacy #cybersecurity</span></div>
<div class="city-img"><img src="images/washington-dc.jpg" alt="Washington D.C."></div>
</div>
<div class="post scheduled">
<div class="meta">
<div class="date">Wed, Mar 18</div>
<div class="time">10:17 AM EDT</div>
<span class="lang-badge en">EN</span>
<div class="status queued">⏳ Scheduled</div>
</div>
<div class="tweet-text">Your password manager can read your passwords. The company running it just chooses not to.
clavitor is a password manager built so the operator cannot — not won't — read your vault. Your AI agents use it from Southeast Asia at local latency. Nobody else sees inside.
Now live in Singapore 🇸🇬 — free till May 1st.
<span class="hashtags">#Singapore #cybersecurity</span></div>
<div class="city-img"><img src="images/singapore.jpg" alt="Singapore"></div>
</div>
<div class="post scheduled">
<div class="meta">
<div class="date">Wed, Mar 18</div>
<div class="time">2:43 PM EDT</div>
<span class="lang-badge zh">ZH</span>
<div class="status queued">⏳ Scheduled</div>
</div>
<div class="tweet-text">你的密码管理器可以读取你的密码。只是公司选择不这么做。
clavitor 是一款密码管理器 — 你的 AI 助手可以使用它,而运营方从架构上无法读取你的密码库。
现已在新加坡上线 🇸🇬 — 5月1日前免费。
<span class="hashtags">#隐私 #网络安全</span></div>
<div class="city-img"><img src="images/singapore.jpg" alt="Singapore"></div>
</div>
<div class="post scheduled">
<div class="meta">
<div class="date">Thu, Mar 19</div>
<div class="time">8:52 AM EDT</div>
<span class="lang-badge en">EN</span>
<div class="status queued">⏳ Scheduled</div>
</div>
<div class="tweet-text">LastPass got breached. Your current password manager can read your vault.
clavitor just landed in Zürich 🇨🇭 — your vault in Europe, your AI agents the only ones with the key. Steal the database. You get ciphertext.
Free till May 1st.
<span class="hashtags">#Switzerland #privacy</span></div>
<div class="city-img"><img src="images/zurich.jpg" alt="Zürich"></div>
</div>
<div class="post scheduled">
<div class="meta">
<div class="date">Thu, Mar 19</div>
<div class="time">11:29 AM EDT</div>
<span class="lang-badge de">DE</span>
<div class="status queued">⏳ Scheduled</div>
</div>
<div class="tweet-text">LastPass wurde gehackt. Dein Passwort-Manager kann deinen Tresor lesen.
clavitor ist jetzt in Zürich 🇨🇭 — deine KI-Agenten haben Zugriff. Niemand sonst. Die Datenbank klauen? Du bekommst Rauschen.
Kostenlos bis 1. Mai.
<span class="hashtags">#Schweiz #Datenschutz</span></div>
<div class="city-img"><img src="images/zurich.jpg" alt="Zürich"></div>
</div>
</div>
</div>
<!-- SAO PAULO -->
<div class="week">
<div class="week-label">São Paulo — Date TBD</div>
<div class="posts">
<div class="post pending">
<div class="meta">
<div class="date">TBD ~morning</div>
<span class="lang-badge en">EN</span>
<div class="status tbd">— Pending</div>
</div>
<div class="tweet-text">Your password manager's server is probably in Virginia. Your vault is readable by whoever runs it.
clavitor just landed in São Paulo 🇧🇷 — your data on your continent, your AI agents the only ones with the key.
Steal the database. You get noise.
Free till May 1st.
<span class="hashtags">#Brazil #AIagents #privacy</span></div>
<div class="city-img"><img src="images/sao-paulo.jpg" alt="São Paulo"></div>
</div>
<div class="post pending">
<div class="meta">
<div class="date">TBD ~afternoon</div>
<span class="lang-badge pt">PT</span>
<div class="status tbd">— Pending</div>
</div>
<div class="tweet-text">O servidor do seu gerenciador de senhas provavelmente está na Virgínia. E o operador pode ler tudo.
clavitor chegou em São Paulo 🇧🇷 — seus dados no seu continente, seus agentes de IA os únicos com a chave.
Roube o banco de dados. Você obtém ruído.
Grátis até 1º de maio.
<span class="hashtags">#Brazil #AIagents #privacidade</span></div>
<div class="city-img"><img src="images/sao-paulo.jpg" alt="São Paulo"></div>
</div>
</div>
</div>
<!-- UPCOMING -->
<div class="week">
<div class="week-label">Upcoming — Sequence TBD</div>
<div class="posts">
<div class="post pending">
<div class="meta"><div class="date">London 🇬🇧</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Your bank is in London. Your law firm runs AI agents on client data. Your password manager's server is in Virginia — and whoever runs it can read every credential.
clavitor just landed in London 🇬🇧 — your agents get access. Nobody else does. Ever.
Free till May 1st. <span class="hashtags">#UK #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/london.jpg" alt="London"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Stockholm 🇸🇪</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Most password managers are built on a promise: "we won't look."
clavitor is built so we can't. Your vault in Stockholm 🇸🇪 — encrypted so the operator sees only ciphertext. Your AI agents are the only key.
Free till May 1st. <span class="hashtags">#Sweden #AIagents #privacy</span></div>
<div class="city-img"><img src="images/stockholm.jpg" alt="Stockholm"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Dubai 🇦🇪</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">The Middle East is spending billions on AI. Your agents need credentials. Those credentials live on a server somebody else can read.
clavitor is live in Dubai 🇦🇪 — the vault your agents use that the operator cannot open. Architecture, not policy.
Free till May 1st. <span class="hashtags">#UAE #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/dubai.jpg" alt="Dubai"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Lagos 🇳🇬</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Africa's AI moment is here. Your agents are running. Your credentials are on a server somebody else controls.
clavitor is live in Lagos 🇳🇬 — your vault on your continent, low latency for African agents. Steal it. You get ciphertext.
Free till May 1st. <span class="hashtags">#Nigeria #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/lagos.jpg" alt="Lagos"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Tokyo 🇯🇵</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Japan's enterprise AI adoption is accelerating. Every agent workflow that touches credentials needs a vault the operator cannot read.
clavitor just landed in Tokyo 🇯🇵 — low latency for your agents, zero access for the operator. Not us, not anyone.
Free till May 1st. <span class="hashtags">#Japan #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/tokyo.jpg" alt="Tokyo"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Mumbai 🇮🇳</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">India has the largest developer community in the world. They're building AI agents. Those agents need credentials. Those credentials need a vault nobody else can read.
clavitor is live in Mumbai 🇮🇳 — low latency for your agents in South Asia, operator-blind. Always.
Free till May 1st. <span class="hashtags">#India #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/mumbai.jpg" alt="Mumbai"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Seoul 🇰🇷</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Samsung, LG, Kakao — Korea's enterprises are deploying AI agents at scale. Every one needs credentials. Every one needs a vault the operator cannot read.
clavitor is live in Seoul 🇰🇷 — local latency for your agents, operator-blind by architecture.
Free till May 1st. <span class="hashtags">#Korea #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/seoul.jpg" alt="Seoul"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Sydney 🇦🇺</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Your AI agents are already running. Is your vault operator-blind?
clavitor is live in Sydney 🇦🇺 — local latency, encrypted vault, agent-ready. The operator sees ciphertext. That's it.
Free till May 1st. <span class="hashtags">#Australia #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/sydney.jpg" alt="Sydney"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Istanbul 🇹🇷</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Turkey's AI startup scene is moving fast. Your agents need credentials that live somewhere the platform can't read.
clavitor is live in Istanbul 🇹🇷 — encrypted so the operator sees nothing. Your agents are the only key.
Free till May 1st. <span class="hashtags">#Turkey #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/istanbul.jpg" alt="Istanbul"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Nairobi 🇰🇪</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Nairobi's builders are skipping the old stack — straight to AI agents. Those agents need credentials that the platform hosting them cannot read.
clavitor is live in Nairobi 🇰🇪 — your vault close to where your agents run. Operator-blind.
Free till May 1st. <span class="hashtags">#Kenya #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/nairobi.jpg" alt="Nairobi"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Cape Town 🇿🇦</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">It doesn't matter where your vault lives if the operator can read it. clavitor just landed in Cape Town 🇿🇦 — your data in-region, encrypted so we see only ciphertext. Subpoena us. We hand over noise.
Free till May 1st. <span class="hashtags">#SouthAfrica #AIagents #privacy</span></div>
<div class="city-img"><img src="images/cape-town.jpg" alt="Cape Town"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Madrid 🇪🇸</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">GDPR says companies must protect your data. It doesn't say they can't read it.
clavitor just landed in Madrid 🇪🇸 — built so the operator architecturally cannot. Your agents use it. Nobody else sees inside.
Free till May 1st. <span class="hashtags">#Spain #AIagents #privacy</span></div>
<div class="city-img"><img src="images/madrid.jpg" alt="Madrid"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Montréal 🇨🇦</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Privacy laws tell companies how to handle your data. They don't stop companies from reading it.
clavitor just landed in Montréal 🇨🇦 — the vault that's operator-blind by design. Your agents get in. We don't.
Free till May 1st. <span class="hashtags">#Canada #AIagents #privacy</span></div>
<div class="city-img"><img src="images/montreal.jpg" alt="Montréal"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Mexico City 🇲🇽</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Latin America's largest city. Its developers are building AI agents at speed. Those agents hand credentials to servers somebody else controls.
clavitor is live in Mexico City 🇲🇽 — operator-blind, agent-ready.
Free till May 1st. <span class="hashtags">#Mexico #AIagents #cybersecurity</span></div>
<div class="city-img"><img src="images/mexico-city.jpg" alt="Mexico City"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Bogotá 🇨🇴</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Your credentials live on a server. That server has an operator. That operator can read your vault.
clavitor is live in Bogotá 🇨🇴 — built so the operator cannot. Steal the database. You get ciphertext.
Free till May 1st. <span class="hashtags">#Colombia #AIagents #privacy</span></div>
<div class="city-img"><img src="images/bogota.jpg" alt="Bogotá"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Santiago 🇨🇱</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Every password manager promises to protect your data. clavitor is built so we can't betray that promise even if we wanted to.
Live in Santiago 🇨🇱 — your agents hold the key. We hold ciphertext.
Free till May 1st. <span class="hashtags">#Chile #AIagents #privacy</span></div>
<div class="city-img"><img src="images/santiago.jpg" alt="Santiago"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Hong Kong 🇭🇰</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">When the rules change overnight, your vault shouldn't be readable by whoever runs the server.
clavitor is live in Hong Kong 🇭🇰 — encrypted so the operator sees only ciphertext. Always. Regardless of what anyone asks.
Free till May 1st. <span class="hashtags">#HongKong #AIagents #privacy</span></div>
<div class="city-img"><img src="images/hong-kong.jpg" alt="Hong Kong"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">San Francisco 🇺🇸</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">You're building agents. You're handing them credentials stored on someone else's server — which that someone can read.
clavitor is live in San Francisco 🇺🇸 — the vault your agents use at West Coast latency, that the operator cannot open.
Free till May 1st. <span class="hashtags">#SF #AIagents #buildinpublic</span></div>
<div class="city-img"><img src="images/san-francisco.jpg" alt="San Francisco"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Buenos Aires 🇦🇷</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Every password manager has one structural problem: the operator can read your vault. Laws don't fix that. Architecture does.
clavitor is live in Buenos Aires 🇦🇷 — operator-blind by design. Your agents hold the key. We hold noise.
Free till May 1st. <span class="hashtags">#Argentina #AIagents #privacy</span></div>
<div class="city-img"><img src="images/buenos-aires.jpg" alt="Buenos Aires"></div>
</div>
<div class="post pending">
<div class="meta"><div class="date">Auckland 🇳🇿</div><span class="lang-badge en">EN</span><div class="status tbd">— Pending</div></div>
<div class="tweet-text">Five Eyes, three eyes, one eye — doesn't matter. clavitor just landed in Auckland 🇳🇿. Subpoena us. We hand over ciphertext. That's not a legal position. It's architecture.
Free till May 1st. <span class="hashtags">#NewZealand #AIagents #privacy</span></div>
<div class="city-img"><img src="images/auckland.jpg" alt="Auckland"></div>
</div>
</div>
</div>
<p style="color:#333; font-size:11px; margin-top:40px; text-align:center;">clavitor drip schedule · George · March 2026</p>
</body>
</html>

View File

@ -0,0 +1,71 @@
# clavitor — Vision Statement
*"If you want to keep a secret, you must also hide it from yourself."*
— George Orwell
We built a password manager that takes that literally.
---
## What we built
A password manager where the operator cannot read your vault. Not policy. Not a promise. Architecture. The server holds your data and nothing else. Steal the database. You get ciphertext.
Your AI agents authenticate against it, retrieve credentials, and operate autonomously. Nobody else sees inside. Not us, not anyone with a subpoena, not a breach.
We open-sourced it. You don't have to trust us — you can read the proof.
---
## The demoralizing lead
clavitor is built to lead in ways that are irrational to chase.
**Global infrastructure no competitor has built**
22 regions. Every continent. Local providers in markets the major clouds don't reach — Lagos, Nairobi, and beyond. No competitor is close. Every month, the gap widens.
But the global network isn't a feature. It's a consequence of the security model. clavitor never caches credentials — not on your laptop, not in the browser extension, not in the agent. Every access is a live fetch from the vault. The credential exists in your possession only for the instant you use it. Nothing sits around to be stolen. That's a stronger guarantee than any incumbent offers — but it means latency is felt on every single credential lookup. An agent making dozens of calls across a workflow feels every millisecond. The 22 POPs exist because zero-cache architecture requires them.
**Architecture the incumbents cannot copy**
1Password, Bitwarden, Dashlane — they can read your vault. They choose not to. To give you what clavitor gives you, they'd have to deprecate every existing vault, migrate every user, and rebuild from scratch. That's not a roadmap item. That's a company-ending decision. They are structurally, permanently trapped.
**The lowest price in the world**
$12/year, hosted. Free to self-host. 1Password charges three times that — and reads your vault. Our price is not a promotion. It's a structural weapon: defensible at scale, irrational to match at zero.
**Agent-native from day one**
Every other password manager was built for humans clicking into browsers. clavitor was built for agents — API-first, MCP-native, designed for autonomous workflows. Everyone else is retrofitting. We shipped the category.
**Open source, Elastic-licensed**
The code is public. Security researchers audit it. Developers integrate it. The community compounds. And the Elastic license means competitors can read every line — but cannot commercially repackage what we built. They can study the map. They cannot copy the territory.
---
## Two types of competition. Neither can win.
**Incumbents** are architecturally trapped. Fixing their fundamental problem destroys their existing product. They know it. We know it. Their users will eventually know it.
**Copycats** face five simultaneous problems with no shortcuts:
- Infrastructure that took years to build across 22 regions with local providers
- Trust that only time-in-market and independent audits produce
- A price floor that requires scale they don't have
- An agent ecosystem and integration surface that compounds daily
- A license that prohibits commercializing our code
The expected value of competing with clavitor is negative. That is not an accident.
---
## Who this is for
Anyone who got a LastPass breach email.
Any developer building AI agents that touch credentials.
Any enterprise running autonomous workflows and asking "who else can see this?"
Any individual who understood what "the company can read your vault" actually means.
The answer to all of them is the same:
**clavitor. The operator cannot read it. Anywhere in the world. At the lowest price. Verifiably.**
---
*clavitor · clavitor.com · @clavitor*

0
commercial/mgmt/.gitkeep Normal file
View File

View File

View File

@ -0,0 +1,17 @@
## Styleguide
**vault1984.css** is the single global stylesheet for all clavitor web surfaces — marketing site (`clavitor-web`) and the app UI (`clavitor`).
- Live: https://clavitor.com/styleguide.html
- Source: `clavitor-web/vault1984.css`
### Rules (no exceptions)
1. **One stylesheet.** `vault1984.css` only. No Tailwind, no inline styles, no `<style>` blocks.
2. **One rule per class.** If you need a variant, add a modifier class (e.g. `.card.gold`), not a new inline style.
3. **One width.** `--width: 1280px` via `.container`. Never hardcode a max-width anywhere else.
4. **One padding.** `--pad: 2rem` via `.container`. Never hardcode horizontal padding.
5. **CSS variables for everything.** Colors, spacing, radius, fonts — all via `var(--*)`.
### To use in clavitor app
Copy or symlink `vault1984.css` into `clavitor/web/` and embed it. The token set (colors, fonts, radius) is shared — app UI should feel identical to the marketing site.

View File

@ -0,0 +1,5 @@
<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>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,3 @@
module github.com/clavitor/clavitor-web
go 1.23.6

128
commercial/website/main.go Normal file
View File

@ -0,0 +1,128 @@
package main
import (
"embed"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
"strings"
)
//go:embed templates/*.tmpl
var tmplFS embed.FS
//go:embed *.svg *.css
var static embed.FS
var templates *template.Template
var devMode bool
type PageData struct {
Page string
Title string
Desc string
ActiveNav string
}
func loadTemplates() {
if devMode {
templates = template.Must(template.ParseGlob("templates/*.tmpl"))
} else {
sub, _ := fs.Sub(tmplFS, "templates")
templates = template.Must(template.ParseFS(sub, "*.tmpl"))
}
}
func render(w http.ResponseWriter, data PageData) {
if devMode {
loadTemplates()
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := templates.ExecuteTemplate(w, "base.tmpl", data); err != nil {
log.Printf("template error: %v", err)
http.Error(w, "Internal error", 500)
}
}
func geoHandler(w http.ResponseWriter, r *http.Request) {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.RemoteAddr
}
if i := strings.LastIndex(ip, ":"); i >= 0 {
ip = ip[:i]
}
ip = strings.Trim(ip, "[]")
resp, err := http.Get("https://ipapi.co/" + ip + "/json/")
if err != nil {
http.Error(w, `{"error":"geo failed"}`, 502)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
io.Copy(w, resp.Body)
}
func main() {
if _, err := os.Stat("templates"); err == nil {
devMode = true
log.Println("dev mode: templates loaded from disk")
}
loadTemplates()
port := os.Getenv("PORT")
if port == "" {
port = "8099"
}
http.HandleFunc("/geo", geoHandler)
http.HandleFunc("/hosted", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "hosted", Title: "clavitor — Hosted", ActiveNav: "hosted"})
})
http.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "install", Title: "Self-host — clavitor", Desc: "Self-host clavitor in 30 seconds. One binary, no dependencies.", ActiveNav: "install"})
})
http.HandleFunc("/pricing", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "pricing", Title: "Pricing — clavitor", Desc: "Free self-hosted or $12/year hosted (launch price). No tiers, no per-seat, no contact sales.", ActiveNav: "pricing"})
})
http.HandleFunc("/privacy", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "privacy", Title: "Privacy Policy — clavitor"})
})
http.HandleFunc("/terms", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "terms", Title: "Terms of Service — clavitor"})
})
http.HandleFunc("/sources", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "sources", Title: "Sources — clavitor"})
})
http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"})
})
// Catch-all: index page at "/" or static files or .html redirects
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
render(w, PageData{Page: "index", Title: "clavitor — AI-native password manager", Desc: "Field-level encryption for password managers that live alongside AI assistants. Your AI gets what it needs. Your secrets stay yours."})
return
}
// Redirect old .html URLs to clean paths
if strings.HasSuffix(r.URL.Path, ".html") {
clean := strings.TrimSuffix(r.URL.Path, ".html")
if clean == "/index" {
clean = "/"
}
http.Redirect(w, r, clean, http.StatusMovedPermanently)
return
}
http.FileServer(http.FS(static)).ServeHTTP(w, r)
})
log.Printf("clavitor-web starting on :%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}

1
commercial/website/tailwind.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
{{if .Desc}}<meta name="description" content="{{.Desc}}">{{end}}
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<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=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/vault1984.css">
{{if eq .Page "install"}}{{template "install-head"}}{{end}}
{{if eq .Page "styleguide"}}{{template "styleguide-head"}}{{end}}
</head>
<body>
<nav class="nav">
<div class="nav-inner">
<a href="/" class="nav-logo">clav<span class="n">itor</span></a>
<div class="nav-links">
<a href="https://github.com/johanjongsma/vault1984" target="_blank" rel="noopener" class="nav-link">GitHub</a>
<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>
<a href="#" class="nav-link btn btn-ghost">Sign in</a>
<a href="/hosted" class="btn btn-primary">Get hosted &mdash; <s>$20</s> $12/yr</a>
</div>
</div>
</nav>
{{if eq .Page "index"}}{{template "index" .}}
{{else if eq .Page "hosted"}}{{template "hosted" .}}
{{else if eq .Page "install"}}{{template "install" .}}
{{else if eq .Page "pricing"}}{{template "pricing" .}}
{{else if eq .Page "privacy"}}{{template "privacy" .}}
{{else if eq .Page "terms"}}{{template "terms" .}}
{{else if eq .Page "sources"}}{{template "sources" .}}
{{else if eq .Page "styleguide"}}{{template "styleguide" .}}
{{end}}
{{if ne .Page "styleguide"}}{{template "footer"}}{{end}}
{{if eq .Page "index"}}{{template "index-script"}}
{{else if eq .Page "hosted"}}{{template "hosted-script"}}
{{end}}
</body>
</html>

View File

@ -0,0 +1,20 @@
{{define "footer"}}
<footer class="footer">
<div class="container">
<div class="footer-inner">
<div class="footer-links">
<a href="/" class="vaultname">clav<span class="n">itor</span></a>
<a href="https://github.com/johanjongsma/vault1984" target="_blank" rel="noopener">GitHub</a>
<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>
</div>
</div>
<p class="footer-copy">Built for humans with AI assistants.</p>
</div>
</footer>
{{end}}

View File

@ -0,0 +1,328 @@
{{define "hosted"}}
<!-- Hero -->
<div class="hero container">
<p class="label accent mb-4"><span class="vaultname">vault<span class="n">1984</span></span> hosted</p>
<h1>Your vault needs to work everywhere. We make sure it does.</h1>
<p class="lead">Your laptop, your phone, your browser extension — at home, at work, on the road. We run the infrastructure across 22 regions. You pick where your data lives. <s>$20</s> $12/yr.</p>
</div>
<!-- Map -->
<div class="section container">
<div class="map-wrap">
<svg id="worldmap" viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
<image href="/worldmap.svg" x="0" y="0" width="1000" height="460"/>
<circle cx="284.7" cy="143.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.00s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.00s" repeatCount="indefinite"/></circle>
<circle cx="284.7" cy="143.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.80s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="0.80s" repeatCount="indefinite"/></circle>
<circle cx="284.7" cy="143.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="284.7" y="155.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Washington D.C.</text>
<circle cx="160.0" cy="143.1" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.08s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.08s" repeatCount="indefinite"/></circle>
<circle cx="160.0" cy="143.1" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/></circle>
<circle cx="160.0" cy="143.1" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="160.0" y="135.1" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">San Francisco</text>
<circle cx="295.6" cy="122.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.16s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.16s" repeatCount="indefinite"/></circle>
<circle cx="295.6" cy="122.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/></circle>
<circle cx="295.6" cy="122.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="295.6" y="114.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Montréal</text>
<circle cx="224.7" cy="187.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.24s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.24s" repeatCount="indefinite"/></circle>
<circle cx="224.7" cy="187.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/></circle>
<circle cx="224.7" cy="187.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="224.7" y="199.0" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Mexico City</text>
<circle cx="294.2" cy="219.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.32s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.32s" repeatCount="indefinite"/></circle>
<circle cx="294.2" cy="219.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/></circle>
<circle cx="294.2" cy="219.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="294.2" y="231.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Bogotá</text>
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.40s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.40s" repeatCount="indefinite"/></circle>
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/></circle>
<circle cx="370.6" cy="282.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="370.6" y="294.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">São Paulo</text>
<circle cx="303.9" cy="306.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.48s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.48s" repeatCount="indefinite"/></circle>
<circle cx="303.9" cy="306.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/></circle>
<circle cx="303.9" cy="306.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="303.9" y="318.0" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Santiago</text>
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.56s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.56s" repeatCount="indefinite"/></circle>
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/></circle>
<circle cx="499.7" cy="106.0" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="499.7" y="98.0" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">London</text>
<circle cx="523.6" cy="117.6" r="5" fill="none" stroke="#D4AF37" stroke-width="2"><animate attributeName="r" values="5;18;5" dur="2.4s" begin="0.64s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.64s" repeatCount="indefinite"/></circle>
<circle cx="523.6" cy="117.6" r="5" fill="none" stroke="#D4AF37" stroke-width="1.5"><animate attributeName="r" values="5;18;5" dur="2.4s" begin="1.44s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.44s" repeatCount="indefinite"/></circle>
<circle cx="523.6" cy="117.6" r="6" fill="#D4AF37" stroke="#0a1628" stroke-width="2"/>
<circle cx="523.6" cy="117.6" r="3" fill="#0a1628"/>
<text x="523.6" y="109.6" font-family="Inter,sans-serif" font-size="8.5" fill="#D4AF37" text-anchor="middle" opacity="0.85">Zürich</text>
<circle cx="489.7" cy="136.4" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.72s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.72s" repeatCount="indefinite"/></circle>
<circle cx="489.7" cy="136.4" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/></circle>
<circle cx="489.7" cy="136.4" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="489.7" y="128.4" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Madrid</text>
<circle cx="550.3" cy="82.1" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.80s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.80s" repeatCount="indefinite"/></circle>
<circle cx="550.3" cy="82.1" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/></circle>
<circle cx="550.3" cy="82.1" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="550.3" y="74.1" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Stockholm</text>
<circle cx="580.3" cy="134.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.88s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.88s" repeatCount="indefinite"/></circle>
<circle cx="580.3" cy="134.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/></circle>
<circle cx="580.3" cy="134.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="580.3" y="126.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Istanbul</text>
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="0.96s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="0.96s" repeatCount="indefinite"/></circle>
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.76s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.76s" repeatCount="indefinite"/></circle>
<circle cx="653.6" cy="173.6" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="653.6" y="165.6" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Dubai</text>
<circle cx="509.4" cy="215.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.04s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.04s" repeatCount="indefinite"/></circle>
<circle cx="509.4" cy="215.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.84s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.84s" repeatCount="indefinite"/></circle>
<circle cx="509.4" cy="215.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="509.4" y="227.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Lagos</text>
<circle cx="602.2" cy="232.8" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.12s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.12s" repeatCount="indefinite"/></circle>
<circle cx="602.2" cy="232.8" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.92s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="1.92s" repeatCount="indefinite"/></circle>
<circle cx="602.2" cy="232.8" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="602.2" y="244.8" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Nairobi</text>
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.20s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.20s" repeatCount="indefinite"/></circle>
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.00s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.00s" repeatCount="indefinite"/></circle>
<circle cx="551.1" cy="307.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="551.1" y="319.2" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Cape Town</text>
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.28s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.28s" repeatCount="indefinite"/></circle>
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.08s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.08s" repeatCount="indefinite"/></circle>
<circle cx="702.5" cy="187.7" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="702.5" y="179.7" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Mumbai</text>
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.36s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.36s" repeatCount="indefinite"/></circle>
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.16s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.16s" repeatCount="indefinite"/></circle>
<circle cx="788.3" cy="227.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="788.3" y="239.2" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Singapore</text>
<circle cx="920.0" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.44s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.44s" repeatCount="indefinite"/></circle>
<circle cx="920.0" cy="307.2" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.24s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.24s" repeatCount="indefinite"/></circle>
<circle cx="920.0" cy="307.2" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="920.0" y="319.2" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Sydney</text>
<circle cx="888.1" cy="148.3" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.52s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.52s" repeatCount="indefinite"/></circle>
<circle cx="888.1" cy="148.3" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.32s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.32s" repeatCount="indefinite"/></circle>
<circle cx="888.1" cy="148.3" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="888.1" y="140.3" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Tokyo</text>
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.60s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.60s" repeatCount="indefinite"/></circle>
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.40s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.40s" repeatCount="indefinite"/></circle>
<circle cx="852.5" cy="143.6" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="852.5" y="135.6" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Seoul</text>
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#22C55E" stroke-width="1.5"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="1.68s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.6;0;0.6" dur="2.4s" begin="1.68s" repeatCount="indefinite"/></circle>
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#22C55E" stroke-width="1"><animate attributeName="r" values="4;13;4" dur="2.4s" begin="2.48s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" begin="2.48s" repeatCount="indefinite"/></circle>
<circle cx="817.2" cy="180.3" r="4.5" fill="#22C55E" stroke="#0a1628" stroke-width="1.5"/>
<text x="817.2" y="172.3" font-family="Inter,sans-serif" font-size="8.5" fill="#6ee7a0" text-anchor="middle" opacity="0.85">Hong Kong</text>
</svg>
</div>
<div class="map-gap"></div>
<div id="dc-grid" class="mb-8">
<!-- Self-hosted -->
<div class="dc-card red" data-lon="-999">
<div class="dc-icon">🖥️</div>
<div class="dc-name">Self-hosted</div>
<div class="dc-sub">Your machine. Your rules.</div>
<div class="dc-status"><span class="dc-dot"></span>Free forever</div>
<a href="/install" class="btn btn-red btn-block">Download now &rarr;</a>
</div>
<!-- Zürich HQ -->
<div class="dc-card gold" data-lon="8.5">
<div class="dc-icon">🇨🇭</div>
<div class="dc-name">Zürich, Switzerland</div>
<div class="dc-sub">Capital of Privacy</div>
<div class="dc-status"><span class="dc-dot"></span>Headquarters</div>
<a href="/signup?region=eu-central-2" class="btn btn-gold btn-block">Buy now &rarr;</a>
</div>
<!-- Closest POP — populated by JS -->
<div id="closest-pop" class="dc-card" data-lon="999">
<div class="dc-icon">📍</div>
<div id="closest-name" class="dc-name">Nearest region</div>
<div id="closest-sub" class="dc-sub">Locating you…</div>
<div class="dc-status"><span class="dc-dot"></span>Closest to you</div>
<a id="closest-buy" href="/signup" class="btn btn-accent btn-block">Buy now &rarr;</a>
</div>
</div>
</div>
<hr class="divider">
<!-- Why Zürich -->
<div class="section container">
<p class="label gold mb-3">Why Zürich</p>
<h2 class="mb-4">Sealed fields: jurisdiction irrelevant.<br>Agent fields: it isn't.</h2>
<p class="lead mb-8">Sealed fields are protected by math — where the server sits doesn't matter. But agent fields live on a server in a jurisdiction. A US server is subject to the CLOUD Act. Zürich, Switzerland is subject to Swiss law — which does not cooperate with foreign government data requests. No backdoors. Both layers protected.</p>
<div class="grid-3">
<div class="card">
<p class="label mb-2">Self-hosted · US</p>
<p>Your server, your rules — until a court says otherwise. CLOUD Act applies to US persons regardless of encryption.</p>
</div>
<div class="card">
<p class="label mb-2">Self-hosted · anywhere</p>
<p>Full control. Your infrastructure, your jurisdiction. The right choice if you know what you're doing.</p>
</div>
<div class="card gold">
<p class="label gold mb-2">Hosted · Zürich, Switzerland</p>
<p>Swiss law. Swiss courts. Capital of Privacy. No CLOUD Act. No backdoors. We handle the infrastructure — you get the protection.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- What hosted adds -->
<div class="section container">
<p class="label accent mb-3">What hosted adds</p>
<h2 class="mb-8">Everything in self-hosted, plus</h2>
<div class="grid-3">
<div class="card alt">
<h3 class="mb-2">Managed infrastructure</h3>
<p>We run it, monitor it, and keep it up. You just use it.</p>
</div>
<div class="card alt">
<h3 class="mb-2">Daily encrypted backups</h3>
<p>Automatic daily backups. Encrypted at rest. Restorable on request.</p>
</div>
<div class="card alt">
<h3 class="mb-2">22 regions</h3>
<p>Pick your region at signup. Your data stays there. Every continent covered.</p>
</div>
<div class="card alt">
<h3 class="mb-2">Automatic updates</h3>
<p>Security patches and new features deployed automatically. No downtime.</p>
</div>
<div class="card alt">
<h3 class="mb-2">TLS included</h3>
<p>HTTPS out of the box. No Caddy, no certbot, no renewal headaches.</p>
</div>
<div class="card alt">
<h3 class="mb-2">Email support</h3>
<p>Real human support. Not a chatbot. Not a forum post into the void.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- CTA -->
<div class="section container">
<h2 class="mb-4">Ready?</h2>
<p class="lead mb-6"><s>$20</s> $12/yr. 7-day money-back. Every feature included.</p>
<div class="btn-row">
<a href="/signup" class="btn btn-primary">Get started</a>
<a href="/pricing" class="btn btn-ghost">Compare plans &rarr;</a>
</div>
</div>
{{end}}
{{define "hosted-script"}}
<script>
(function() {
const W = 1000, H = 460;
function project(lon, lat) {
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
const miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
const maxMiller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*80*Math.PI/180));
const x = (lon + 180) / 360 * W;
const y = H/2 - (miller / (2*maxMiller)) * H;
return [Math.round(x*10)/10, Math.round(y*10)/10];
}
function addVisitorDot(lat, lon, city) {
const svg = document.getElementById('worldmap');
if (!svg) return;
const [x, y] = project(lon, lat);
const ns = 'http://www.w3.org/2000/svg';
// Pulse ring
const ring = document.createElementNS(ns, 'circle');
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', '#EF4444'); ring.setAttribute('stroke-width', '1.5');
const a1 = document.createElementNS(ns, 'animate');
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
const a2 = document.createElementNS(ns, 'animate');
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.8;0;0.8');
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
ring.appendChild(a1); ring.appendChild(a2);
// Dot
const dot = document.createElementNS(ns, 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#EF4444');
dot.setAttribute('stroke', '#0a1628'); dot.setAttribute('stroke-width', '1.5');
// Label
const label = document.createElementNS(ns, 'text');
label.setAttribute('x', x); label.setAttribute('y', y + 15);
label.setAttribute('font-family', 'Inter,sans-serif');
label.setAttribute('font-size', '10');
label.setAttribute('fill', '#EF4444');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('font-weight', '500');
label.textContent = city || 'You';
svg.appendChild(ring);
svg.appendChild(dot);
svg.appendChild(label);
}
const POPS = [
{name:"Washington D.C.", region:"us-east-1", lat:37.5, lon:-77.5},
{name:"San Francisco", region:"us-west-1", lat:37.8, lon:-122.4},
{name:"Montréal", region:"ca-central-1", lat:45.5, lon:-73.6},
{name:"Mexico City", region:"mx-central-1", lat:19.4, lon:-99.1},
{name:"Bogotá", region:"sa-bogota", lat:4.7, lon:-74.1},
{name:"São Paulo", region:"sa-east-1", lat:-23.6, lon:-46.6},
{name:"Santiago", region:"sa-west-1", lat:-33.4, lon:-70.6},
{name:"London", region:"eu-west-2", lat:51.5, lon:-0.1},
{name:"Zürich", region:"eu-central-2", lat:47.4, lon:8.5},
{name:"Madrid", region:"eu-south-2", lat:40.4, lon:-3.7},
{name:"Stockholm", region:"eu-north-1", lat:59.3, lon:18.1},
{name:"Istanbul", region:"tr-west-1", lat:41.0, lon:28.9},
{name:"Dubai", region:"me-central-1", lat:25.2, lon:55.3},
{name:"Lagos", region:"af-west-1", lat:6.5, lon:3.4},
{name:"Nairobi", region:"af-east-1", lat:-1.3, lon:36.8},
{name:"Cape Town", region:"af-south-1", lat:-33.9, lon:18.4},
{name:"Mumbai", region:"ap-south-1", lat:19.1, lon:72.9},
{name:"Singapore", region:"ap-southeast-1", lat:1.3, lon:103.8},
{name:"Sydney", region:"ap-southeast-2", lat:-33.9, lon:151.2},
{name:"Tokyo", region:"ap-northeast-1", lat:35.7, lon:139.7},
{name:"Seoul", region:"ap-northeast-2", lat:37.6, lon:126.9},
{name:"Hong Kong", region:"ap-east-1", lat:22.3, lon:114.2},
];
function findClosestPop(lat, lon) {
return POPS.reduce((best, p) => {
const d = (lat-p.lat)**2 + (lon-p.lon)**2;
const bd = (lat-best.lat)**2 + (lon-best.lon)**2;
return d < bd ? p : best;
});
}
function handleGeoData(d) {
if (!d.latitude || !d.longitude) return;
addVisitorDot(d.latitude, d.longitude, d.city || 'You');
const closest = findClosestPop(d.latitude, d.longitude);
const nameEl = document.getElementById('closest-name');
const subEl = document.getElementById('closest-sub');
const buyEl = document.getElementById('closest-buy');
if (nameEl) nameEl.textContent = closest.name;
if (subEl) subEl.textContent = d.city ? `~${d.city}` : 'Your region';
if (buyEl) buyEl.href = `/signup?region=${closest.region}`;
}
// Ask browser geolocation first (accurate, triggers permission prompt)
// Fall back to server-side IP lookup if denied or unavailable
function tryIPGeo() {
fetch('/geo')
.then(r => r.json())
.then(d => { if (d.latitude) handleGeoData(d); })
.catch(() => {});
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
pos => {
handleGeoData({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
city: '', region: '', country_name: '', country_code: ''
});
},
() => tryIPGeo() // denied — fall back to IP
);
} else {
tryIPGeo();
}
})();
</script>
{{end}}

View File

@ -0,0 +1,509 @@
{{define "index"}}
<!-- Hero -->
<div class="container hero-split">
<div>
<p class="label accent mb-6">George Orwell &mdash; 1984</p>
<h1 class="mb-6">"If you want to keep a secret, you must also hide it from yourself."</h1>
<p class="lead mb-3">The only password manager for AI agents worth your trust.</p>
<p class="mb-6">We cannot read your passwords. Not <em>will not</em>. <strong>Cannot.</strong> Your Credential and Identity keys are derived from your hardware authenticator and baked into your tokens. Our servers hold ciphertext they have never had the means to decrypt.</p>
<div class="btn-row">
<a href="/hosted" class="btn btn-primary">Get hosted &mdash; <s>$20</s> $12/yr</a>
<a href="/install" class="btn btn-ghost">Self-host free &rarr;</a>
</div>
</div>
<div>
<!-- Hero SVG: Credential / Identity split -->
<svg viewBox="0 0 480 380" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect x="0" y="0" width="480" height="380" rx="12" fill="#111f38"/>
<!-- Top labels -->
<text x="130" y="35" font-family="JetBrains Mono, monospace" font-size="11" fill="#94a3b8" text-anchor="middle">AI Agent</text>
<text x="350" y="35" font-family="JetBrains Mono, monospace" font-size="11" fill="#94a3b8" text-anchor="middle">You only</text>
<!-- Arrows -->
<path d="M130 42 L130 58" stroke="#22C55E" stroke-width="1.5" marker-end="url(#arrowGreen)"/>
<path d="M350 42 L350 58" stroke="#EF4444" stroke-width="1.5" marker-end="url(#arrowRed)"/>
<defs>
<marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="4" refY="3" orient="auto"><path d="M0,0 L4,3 L0,6" fill="none" stroke="#22C55E" stroke-width="1.5"/></marker>
<marker id="arrowRed" markerWidth="8" markerHeight="6" refX="4" refY="3" orient="auto"><path d="M0,0 L4,3 L0,6" fill="none" stroke="#EF4444" stroke-width="1.5"/></marker>
</defs>
<!-- Credential Column -->
<rect x="30" y="65" width="200" height="260" rx="8" fill="none" stroke="#22C55E" stroke-width="1" stroke-opacity="0.3"/>
<rect x="30" y="65" width="200" height="30" rx="8" fill="#22C55E" fill-opacity="0.1"/>
<text x="130" y="82" font-family="JetBrains Mono, monospace" font-size="10" fill="#22C55E" text-anchor="middle" font-weight="600">Credential Encryption</text>
<!-- Credential items -->
<g>
<rect x="50" y="115" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="138" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">github_token</text>
<circle cx="192" cy="133" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 133 L190.5 135.5 L196 130" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="50" y="163" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="186" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">ssh_key</text>
<circle cx="192" cy="181" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 181 L190.5 183.5 L196 178" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="50" y="211" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="234" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">totp_github</text>
<circle cx="192" cy="229" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 229 L190.5 231.5 L196 226" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="50" y="259" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="282" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">oauth_slack</text>
<circle cx="192" cy="277" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 277 L190.5 279.5 L196 274" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Identity Column -->
<rect x="250" y="65" width="200" height="260" rx="8" fill="none" stroke="#EF4444" stroke-width="1" stroke-opacity="0.3"/>
<rect x="250" y="65" width="200" height="30" rx="8" fill="#EF4444" fill-opacity="0.1"/>
<text x="350" y="82" font-family="JetBrains Mono, monospace" font-size="10" fill="#EF4444" text-anchor="middle" font-weight="600">Identity Encryption</text>
<!-- Identity items -->
<g>
<rect x="270" y="115" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="138" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">credit_card</text>
<rect x="408" y="125" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 131 L413 135 M416 131 L416 135 M411 133 L411 129 Q411 127 413 127 L416 127 Q418 127 418 129 L418 133 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="270" y="163" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="186" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">cvv</text>
<rect x="408" y="173" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 179 L413 183 M416 179 L416 183 M411 181 L411 177 Q411 175 413 175 L416 175 Q418 175 418 177 L418 181 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="270" y="211" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="234" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">passport</text>
<rect x="408" y="221" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 227 L413 231 M416 227 L416 231 M411 229 L411 225 Q411 223 413 223 L416 223 Q418 223 418 225 L418 229 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="270" y="259" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="282" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">ssn</text>
<rect x="408" y="269" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 275 L413 279 M416 275 L416 279 M411 277 L411 273 Q411 271 413 271 L416 271 Q418 271 418 273 L418 277 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Center vault icon -->
<rect x="224" y="340" width="32" height="28" rx="4" fill="#111f38" stroke="#94a3b8" stroke-width="1"/>
<circle cx="240" cy="352" r="3" fill="none" stroke="#94a3b8" stroke-width="1"/>
<line x1="240" y1="355" x2="240" y2="360" stroke="#94a3b8" stroke-width="1"/>
</svg>
</div>
</div>
<hr class="divider">
<!-- The Problem -->
<div class="section container">
<h2 class="mb-4">The problem with every other password manager</h2>
<p class="lead mb-8">In 2022, attackers stole the entire vault database of a leading password manager. The encryption worked exactly as designed. Accounts are still being drained today — four years later. The problem was never the encryption. It was who held the key.</p>
<div class="grid-3">
<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="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/></svg></div>
<h3 class="mb-3">They hold the key</h3>
<p>Every major password manager encrypts your vault — then stores a way to decrypt it. The company's servers have always had what it takes to read your passwords. You just hoped they wouldn't.</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="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>
</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>
</div>
</div>
</div>
<hr class="divider">
<!-- How it works -->
<div class="section container">
<p class="label mb-4">How it works</p>
<h2 class="mb-6">"Your assistant can book your flights.<br><span class="gradient-text">Not read your diary.</span>"</h2>
<p class="lead mb-4">Everything in your vault is encrypted. What differs is who holds the key.</p>
<p class="mb-8">Vault Encryption covers everything — the server uses it to store and serve your data. Credential and Identity fields go further: those keys are derived from your hardware authenticator, baked into your tokens, and never sent to our servers. We hold the vault. We never got those keys.</p>
<div class="grid-3">
<div class="card alt">
<span class="badge mb-4" style="background:rgba(148,163,184,0.1);color:#94a3b8">Vault Encryption</span>
<h3 class="mb-3">Titles, URLs, usernames</h3>
<p class="mb-4">Server-readable. Encrypted at rest with the vault key. Needed for search, entry listing, and MCP tool responses.</p>
<ul class="checklist">
<li>Entry names &amp; labels</li>
<li>URLs</li>
<li>Usernames</li>
<li>Tags &amp; metadata</li>
</ul>
</div>
<div class="card alt">
<span class="badge accent mb-4">Credential Encryption</span>
<h3 class="mb-3">AI-readable secrets</h3>
<p class="mb-4">Encrypted client-side. The key lives in your agent tokens. The server stores ciphertext it has never had the means to decrypt.</p>
<ul class="checklist">
<li>API keys &amp; tokens</li>
<li>SSH keys</li>
<li>TOTP 2FA seeds — AI generates codes</li>
<li>OAuth tokens</li>
</ul>
</div>
<div class="card red">
<span class="badge red mb-4">Identity Encryption</span>
<h3 class="mb-3">Touch ID only</h3>
<p class="mb-4">Encrypted client-side with a key derived from your WebAuthn PRF. Hardware tap required. Not in tokens. Agents receive <code>[Identity Encryption — hardware key required]</code>.</p>
<ul class="checklist red">
<li>Credit card numbers &amp; CVV</li>
<li>Passport &amp; government IDs</li>
<li>Social security numbers</li>
<li>Seed phrases &amp; private keys</li>
</ul>
</div>
</div>
</div>
<hr class="divider">
<!-- Features -->
<div class="section container">
<h2 class="mb-4">Built different</h2>
<p class="lead mb-8">Not another password manager with an AI checkbox. The architecture is the feature.</p>
<div class="grid-3">
<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="M4 6h16M4 12h8m-8 6h16"/></svg></div>
<h3 class="mb-3">Field-level encryption tiers</h3>
<p>Each field carries its own encryption layer. Your AI reads the API key, not the CVV. Same entry, different keys, different access.</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="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4"/></svg></div>
<h3 class="mb-3">WebAuthn PRF</h3>
<p>Credential and Identity keys derive from your biometric hardware — Touch ID, Face ID, YubiKey, Titan Key. No master password. No server-side key storage. Math, not policy.</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="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 during a deployment.</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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg></div>
<h3 class="mb-3">Client-side encryption</h3>
<p>Your browser and agents encrypt before anything leaves the client. The vault stores what it receives. It has never seen your Credential or Identity plaintext.</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>
<h3 class="mb-3">One binary, one file</h3>
<p>No Docker. No Postgres. No Redis. One Go binary, one SQLite file. Runs on a Raspberry Pi. Runs on a $4/month VPS. Port 1984.</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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg></div>
<h3 class="mb-3">Smart import</h3>
<p>Native parsers for Chrome, Firefox, Bitwarden, Proton Pass. Any other format via LLM. Auto-detects which fields should be Credential vs Identity. Collision resolution by modification date.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- For your agents -->
<div class="section container">
<div class="grid-2">
<div>
<h2 class="mb-4">Your agents get in.<br><span class="gradient-text">Nobody else gets anything.</span></h2>
<p class="lead mb-6">MCP-native from day one. Every AI agent that supports MCP can read credentials, fetch API keys, and generate 2FA codes — using only what it needs.</p>
<p class="mb-6">Each token is a combined credential: MCP auth and the Credential private key, wrapped together. The agent decrypts locally. The server sees a request and returns ciphertext. Nothing in between.</p>
<div class="code-block">
<p class="code-label">~/.claude/mcp.json</p>
<pre>{
"mcpServers": {
"clavitor": {
"url": "http://localhost:1984/mcp",
"headers": {
"Authorization": "Bearer <span class="prompt">v1984_...</span>"
}
}
}
}</pre>
</div>
</div>
<div>
<div class="card alt mb-4">
<p class="label accent mb-3">MCP Tools</p>
<ul class="checklist">
<li><code>get_credential</code> &mdash; fetch by query</li>
<li><code>list_credentials</code> &mdash; list accessible entries</li>
<li><code>get_totp</code> &mdash; live 2FA code</li>
<li><code>search_vault</code> &mdash; full-text search</li>
<li><code>check_expiring</code> &mdash; upcoming expirations</li>
<li><code>save_credential</code> &mdash; write (explicit permission only)</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">Identity Encryption fields</p>
<p>Agents receive <code>[Identity Encryption — hardware key required]</code> for card numbers, passports, and government IDs. They know the field exists. They cannot read it. Ever.</p>
</div>
</div>
</div>
</div>
<hr class="divider">
<!-- Access Methods -->
<div class="section container">
<h2 class="mb-4">Your agent and you — same vault, right access</h2>
<p class="lead mb-8">Four ways in. Each 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>
<h3 class="mb-2">For AI agents</h3>
<p>Claude Code, Codex, Cursor, or any MCP-compatible agent. Search credentials, fetch API keys, complete 2FA flows — without asking you.</p>
</div>
<div class="card card-hover">
<p class="label accent mb-3">Extension</p>
<h3 class="mb-2">For humans in a browser</h3>
<p>Autofill passwords, generate 2FA codes inline. LLM field mapping fills by intent, not CSS selector — works on SPAs, obfuscated forms, multi-step flows.</p>
</div>
<div class="card card-hover">
<p class="label accent mb-3">CLI</p>
<h3 class="mb-2">For terminal workflows</h3>
<p>Pipe credentials into scripts and CI pipelines. <code>vault get github.token</code> — done.</p>
</div>
<div class="card card-hover">
<p class="label accent mb-3">API</p>
<h3 class="mb-2">For everything else</h3>
<p>REST API with read/write tokens. Full audit log: every access tagged by actor — web, extension, MCP, or agent name.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- The competition -->
<div class="section container">
<p class="label mb-4">Real complaints. Real users.</p>
<h2 class="mb-4">We listened. And addressed them all.</h2>
<p class="lead mb-8">Verbatim quotes from real users — pulled from public forums, GitHub issues, and community threads. Not cherry-picked. Not paraphrased.</p>
<div class="grid-3">
<div class="card red">
<p class="label red mb-3">Leading password manager — Community Forum</p>
<p><em>"The web extensions are laughably bad at this point. This has been going on for months. They either won't fill, wont' unlock, or just plain won't do anything (even clicking extension icon). It's so bad"</em></p>
<p class="mt-2 text-sm" style="color:#64748b">Community forum, April 2024</p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: No desktop app dependency. The extension calls the vault binary directly — no IPC, no sync layer, no unlock chain.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">Leading password manager — GitHub Issues</p>
<p><em>"Every single website loads slower. From Google, up to social media websites like Reddit, Instagram, X... Even scrolling and animation stutters sometimes. So for me the browser extension is unusable. It interferes with my browsing experience like malware."</em></p>
<p class="mt-2 text-sm" style="color:#64748b">GitHub Issues, 2024</p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: Zero content scripts. The extension injects nothing into pages — fills via browser autofill API, on demand, when you ask.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">Leading password manager — Hacker News</p>
<p><em>"The fact they're drip-feeding how bad this breach actually was is terrible enough... Personally I'm never touching them again."</em></p>
<p class="mt-2 text-sm" style="color:#64748b">Hacker News, January 2023</p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: Credential and Identity fields are encrypted before they reach us. We mathematically cannot read them — and neither can anyone who breaches our servers.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">Leading password manager — Community Forum</p>
<p><em>"Since doing so, it asks me to enter my password every 10 minutes or so in the chrome extension"</em></p>
<p class="mt-2 text-sm" style="color:#64748b">Community forum, November 2022</p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: WebAuthn-first. Touch ID is the primary unlock. Session lives locally — no server-side token expiry forcing re-auth every few minutes.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">Leading password manager — Community Forums</p>
<p><em>"the password not only auto-filled in the password field, but also auto-filled in reddit's search box!"</em></p>
<p class="mt-2 text-sm" style="color:#64748b">Community forum</p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: LLM field mapping. The extension reads the form and asks the model which field is which — fills by intent, not by CSS selector guesswork.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">Leading password manager — Community Forums</p>
<p><em>"Bitwarden REFUSES to autofill the actual password saved for a given site or app...and instead fills an old password. It simply substitutes the OLD password for the new one that is plainly saved in the vault."</em></p>
<p class="mt-2 text-sm" style="color:#64748b">Community forum</p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: LLM field mapping matches by intent. Entries are indexed by URL — the right credential, every time.</li>
</ul>
</div>
</div>
<p class="mt-8">All quotes verbatim from public posts. <a href="/sources">View sources &rarr;</a></p>
</div>
<hr class="divider">
<!-- Hosted CTA -->
<div class="section container">
<h2 class="mb-4">Your vault needs to be everywhere you are.</h2>
<p class="lead mb-3">A password manager that only works on your home network isn't a password manager. Your laptop moves. Your phone moves. Your agents run on servers in three time zones.</p>
<p class="mb-3">Self-hosting means a server with a public IP, DNS, TLS certificates, uptime monitoring, and backups. That's not a weekend project — that's infrastructure.</p>
<p class="mb-8">We run <span class="vaultname">clav<span class="n">itor</span></span> across regions on every continent. <s>$20</s> $12/yr. Your Credential and Identity keys never leave your browser — we cannot read your private fields. Not policy. Math.</p>
<div class="btn-row">
<a href="/hosted" class="btn btn-primary">Get hosted &rarr;</a>
<a href="/install" class="btn btn-ghost">Self-host anyway</a>
</div>
</div>
<hr class="divider">
<!-- Quick install -->
<div class="section container">
<h2 class="mb-4">Up and running in 30 seconds</h2>
<p class="lead mb-8">One command. No dependencies.</p>
<div class="code-block mb-6">
<p class="code-label">Terminal</p>
<div><span class="comment"># Self-host in 30 seconds</span></div>
<div><span class="prompt">$</span> curl -fsSL clavitor.com/install.sh | sh</div>
<div><span class="prompt">$</span> clavitor</div>
<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">v1984_your_token_here</span>" }
}
}
}</pre>
</div>
<p class="mt-4"><a href="/install" class="btn btn-accent">Full install guide &rarr;</a></p>
</div>
{{end}}
{{define "index-script"}}
<script>
(function() {
const W = 1000, H = 460;
function project(lon, lat) {
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
const miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
const maxMiller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*80*Math.PI/180));
const x = (lon + 180) / 360 * W;
const y = H/2 - (miller / (2*maxMiller)) * H;
return [Math.round(x*10)/10, Math.round(y*10)/10];
}
function addVisitorDot(lat, lon, city) {
const svg = document.getElementById('worldmap');
if (!svg) return;
const [x, y] = project(lon, lat);
const ns = 'http://www.w3.org/2000/svg';
const ring = document.createElementNS(ns, 'circle');
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', '#EF4444'); ring.setAttribute('stroke-width', '1.5');
const a1 = document.createElementNS(ns, 'animate');
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
const a2 = document.createElementNS(ns, 'animate');
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.8;0;0.8');
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
ring.appendChild(a1); ring.appendChild(a2);
const dot = document.createElementNS(ns, 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#EF4444');
dot.setAttribute('stroke', '#0a1628'); dot.setAttribute('stroke-width', '1.5');
const label = document.createElementNS(ns, 'text');
label.setAttribute('x', x); label.setAttribute('y', y + 15);
label.setAttribute('font-family', 'Inter,sans-serif');
label.setAttribute('font-size', '10');
label.setAttribute('fill', '#EF4444');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('font-weight', '500');
label.textContent = city || 'You';
svg.appendChild(ring);
svg.appendChild(dot);
svg.appendChild(label);
}
function handleGeoData(d) {
if (!d.latitude || !d.longitude) return;
addVisitorDot(d.latitude, d.longitude, d.city || 'You');
const grid = document.getElementById('dc-grid');
if (!grid) return;
const flag = d.country_code ? d.country_code.toUpperCase().split('').map(c =>
String.fromCodePoint(c.charCodeAt(0) + 127397)).join('') : '📍';
const label = [d.city, d.country_name].filter(Boolean).join(', ') || 'Your location';
const region = d.region || '';
const card = document.createElement('div');
card.className = 'rounded-xl p-5 text-center card-hover';
card.setAttribute('data-lon', d.longitude);
card.style.cssText = 'background:#1f0a0a;border:1px solid rgba(239,68,68,0.35)';
card.innerHTML = `
<div class="text-2xl mb-2">${flag}</div>
<div class="text-white font-semibold text-sm">${label}</div>
<div class="text-gray-500 text-xs mb-2">${region}</div>
<div class="flex items-center justify-center gap-1.5 text-xs text-gray-400">
<span class="w-1.5 h-1.5 rounded-full inline-block" style="background:#EF4444;opacity:0.6"></span>You are here
</div>`;
grid.style.gridTemplateColumns = "repeat(5,1fr)";
const cards = [...grid.children];
const insertBefore = cards.find(c => parseFloat(c.getAttribute('data-lon')) > d.longitude);
if (insertBefore) grid.insertBefore(card, insertBefore);
else grid.appendChild(card);
}
fetch('/geo')
.then(r => r.json())
.then(d => {
if (d.latitude) {
handleGeoData(d);
} else if (d.private && navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude, lon = pos.coords.longitude;
fetch(`/geo?lat=${lat}&lon=${lon}`)
.then(r => r.json())
.then(g => handleGeoData({
latitude: lat, longitude: lon,
city: g.city || 'You',
region: g.region || '',
country_name: g.country_name || '',
country_code: g.country_code || ''
}))
.catch(() => handleGeoData({ latitude: lat, longitude: lon,
city: 'You', region: '', country_name: '', country_code: '' }));
}, () => {});
}
})
.catch(() => {});
})();
</script>
{{end}}

View File

@ -0,0 +1,548 @@
{{define "index"}}
<!-- Hero -->
<div class="container hero-split">
<div>
<p class="label accent mb-6">George Orwell &mdash; 1984</p>
<h1 class="mb-6">"If you want to keep a secret, you must also hide it from yourself."</h1>
<p class="lead mb-6">We did. Your Sealed key is derived in your browser from your Touch ID. Our servers have never seen it. They could not decrypt your private fields even if they wanted to. Or anybody else.</p>
<div class="btn-row">
<a href="/hosted" class="btn btn-primary">Get hosted &mdash; <s>$20</s> $12/yr</a>
<a href="/install" class="btn btn-ghost">Self-host free &rarr;</a>
</div>
</div>
<div>
<!-- Hero SVG: L1/L2 split diagram -->
<svg viewBox="0 0 480 380" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect x="0" y="0" width="480" height="380" rx="12" fill="#111f38"/>
<!-- Top labels -->
<text x="130" y="35" font-family="JetBrains Mono, monospace" font-size="11" fill="#94a3b8" text-anchor="middle">AI Agent</text>
<text x="350" y="35" font-family="JetBrains Mono, monospace" font-size="11" fill="#94a3b8" text-anchor="middle">You only</text>
<!-- Arrows -->
<path d="M130 42 L130 58" stroke="#22C55E" stroke-width="1.5" marker-end="url(#arrowGreen)"/>
<path d="M350 42 L350 58" stroke="#EF4444" stroke-width="1.5" marker-end="url(#arrowRed)"/>
<defs>
<marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="4" refY="3" orient="auto"><path d="M0,0 L4,3 L0,6" fill="none" stroke="#22C55E" stroke-width="1.5"/></marker>
<marker id="arrowRed" markerWidth="8" markerHeight="6" refX="4" refY="3" orient="auto"><path d="M0,0 L4,3 L0,6" fill="none" stroke="#EF4444" stroke-width="1.5"/></marker>
</defs>
<!-- L1 Column -->
<rect x="30" y="65" width="200" height="260" rx="8" fill="none" stroke="#22C55E" stroke-width="1" stroke-opacity="0.3"/>
<rect x="30" y="65" width="200" height="30" rx="8" fill="#22C55E" fill-opacity="0.1"/>
<text x="130" y="85" font-family="JetBrains Mono, monospace" font-size="12" fill="#22C55E" text-anchor="middle" font-weight="600">L1 — AI can read</text>
<!-- L1 items -->
<g>
<rect x="50" y="115" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="138" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">github_token</text>
<circle cx="192" cy="133" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 133 L190.5 135.5 L196 130" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="50" y="163" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="186" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">ssh_key</text>
<circle cx="192" cy="181" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 181 L190.5 183.5 L196 178" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="50" y="211" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="234" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">totp_github</text>
<circle cx="192" cy="229" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 229 L190.5 231.5 L196 226" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="50" y="259" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="80" y="282" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">oauth_slack</text>
<circle cx="192" cy="277" r="8" fill="#22C55E" fill-opacity="0.15"/>
<path d="M188 277 L190.5 279.5 L196 274" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- L2 Column -->
<rect x="250" y="65" width="200" height="260" rx="8" fill="none" stroke="#EF4444" stroke-width="1" stroke-opacity="0.3"/>
<rect x="250" y="65" width="200" height="30" rx="8" fill="#EF4444" fill-opacity="0.1"/>
<text x="350" y="85" font-family="JetBrains Mono, monospace" font-size="12" fill="#EF4444" text-anchor="middle" font-weight="600">L2 — you only</text>
<!-- L2 items -->
<g>
<rect x="270" y="115" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="138" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">credit_card</text>
<rect x="408" y="125" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 131 L413 135 M416 131 L416 135 M411 133 L411 129 Q411 127 413 127 L416 127 Q418 127 418 129 L418 133 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="270" y="163" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="186" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">cvv</text>
<rect x="408" y="173" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 179 L413 183 M416 179 L416 183 M411 181 L411 177 Q411 175 413 175 L416 175 Q418 175 418 177 L418 181 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="270" y="211" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="234" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">passport</text>
<rect x="408" y="221" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 227 L413 231 M416 227 L416 231 M411 229 L411 225 Q411 223 413 223 L416 223 Q418 223 418 225 L418 229 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g>
<rect x="270" y="259" width="160" height="36" rx="6" fill="#0A1628"/>
<text x="300" y="282" font-family="JetBrains Mono, monospace" font-size="11" fill="#d1d5db">ssn</text>
<rect x="408" y="269" width="16" height="16" rx="3" fill="#EF4444" fill-opacity="0.15"/>
<path d="M413 275 L413 279 M416 275 L416 279 M411 277 L411 273 Q411 271 413 271 L416 271 Q418 271 418 273 L418 277 Z" stroke="#EF4444" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Center vault icon -->
<rect x="224" y="340" width="32" height="28" rx="4" fill="#111f38" stroke="#94a3b8" stroke-width="1"/>
<circle cx="240" cy="352" r="3" fill="none" stroke="#94a3b8" stroke-width="1"/>
<line x1="240" y1="355" x2="240" y2="360" stroke="#94a3b8" stroke-width="1"/>
</svg>
</div>
</div>
<hr class="divider">
<!-- The Problem -->
<div class="section container">
<h2 class="mb-4">The problem</h2>
<p class="lead mb-8">Every password manager was built before AI agents existed. Now they need to catch up.</p>
<div class="grid-3">
<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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg></div>
<h3 class="mb-3">All-or-nothing is broken</h3>
<p>All others give your AI agent access to everything in your vault, or nothing at all. Your AI needs your GitHub token — it shouldn't also see your passport number.</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="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 security</h3>
<p>"AI-safe" vaults still decrypt everything server-side. If the server can read it, it's not truly private. Math beats policy every time.</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">Agents need credentials — and 2FA</h3>
<p>Your AI can't log in, pass two-factor, or rotate keys without access. <span class="vaultname">clav<span class="n">itor</span></span> lets it do all three — without exposing your credit card to the same pipeline.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- How it works -->
<div class="section container">
<p class="label mb-4">How it works</p>
<h2 class="mb-6">"Your assistant can book your flights.<br><span class="gradient-text">Not read your diary.</span>"</h2>
<p class="lead mb-8">Every field is encrypted. But some get a second lock. That second key is derived from your fingerprint and only exists in your browser. We hold the safe. Only you hold that key.</p>
<div class="grid-2">
<div class="card alt">
<span class="badge accent mb-4">Agent fields</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>
<ul class="checklist">
<li>API keys &amp; tokens</li>
<li>SSH keys</li>
<li>TOTP 2FA codes &mdash; AI generates them for you</li>
<li>OAuth tokens</li>
<li>Structured notes</li>
</ul>
</div>
<div class="card red">
<span class="badge red mb-4">Sealed fields</span>
<h3 class="mb-3">Touch ID only</h3>
<p class="mb-4">Encrypted client-side with WebAuthn PRF. The server never sees the plaintext. Ever.</p>
<ul class="checklist red">
<li>Credit card numbers</li>
<li>CVV</li>
<li>Passport &amp; SSN</li>
<li>Private signing keys</li>
<li>Private notes</li>
</ul>
</div>
</div>
</div>
<hr class="divider">
<!-- Features -->
<div class="section container">
<h2 class="mb-4">Built different</h2>
<p class="lead mb-8">Not another password manager with an AI checkbox. The architecture is the feature.</p>
<div class="grid-3">
<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="M4 6h16M4 12h8m-8 6h16"/></svg></div>
<h3 class="mb-3">Field-level AI visibility</h3>
<p>Each field has its own encryption tier. Your AI reads the username, not the CVV. Same entry, different access.</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="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4"/></svg></div>
<h3 class="mb-3">WebAuthn PRF</h3>
<p>Sealed encryption uses WebAuthn PRF — a cryptographic key derived from your biometric hardware. Math, not policy. We literally cannot decrypt it.</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="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 Agent fields. Your AI generates time-based codes on demand via MCP — 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>
</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>
<h3 class="mb-3">One binary, one file</h3>
<p>No Docker. No Postgres. No Redis. One Go binary, one SQLite file. Runs on a Raspberry Pi. Runs on a $4/month VPS.</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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg></div>
<h3 class="mb-3">LLM field mapping</h3>
<p>Import from any password manager. The built-in LLM automatically classifies which fields should be Agent vs Sealed.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- Multi-agent swarms -->
<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>
<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>
</div>
</div>
<div>
<!-- Multi-agent SVG -->
<svg viewBox="0 0 400 360" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Center vault -->
<rect x="160" y="140" width="80" height="80" rx="12" fill="#111f38" stroke="#94a3b8" stroke-width="1.5"/>
<text x="200" y="175" font-family="JetBrains Mono, monospace" font-size="10" fill="#94a3b8" text-anchor="middle">vault</text>
<text x="200" y="195" font-family="JetBrains Mono, monospace" font-size="14" fill="white" text-anchor="middle" font-weight="600">1984</text>
<!-- Agent 1 — dev -->
<circle cx="80" cy="60" r="32" fill="#22C55E" fill-opacity="0.08" stroke="#22C55E" stroke-width="1"/>
<text x="80" y="56" font-family="JetBrains Mono, monospace" font-size="9" fill="#22C55E" text-anchor="middle">Agent 1</text>
<text x="80" y="68" font-family="JetBrains Mono, monospace" font-size="8" fill="#94a3b8" text-anchor="middle">dev</text>
<line x1="108" y1="80" x2="165" y2="145" stroke="#22C55E" stroke-width="1" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- Agent 2 — social -->
<circle cx="320" cy="60" r="32" fill="#22C55E" fill-opacity="0.08" stroke="#22C55E" stroke-width="1"/>
<text x="320" y="56" font-family="JetBrains Mono, monospace" font-size="9" fill="#22C55E" text-anchor="middle">Agent 2</text>
<text x="320" y="68" font-family="JetBrains Mono, monospace" font-size="8" fill="#94a3b8" text-anchor="middle">social</text>
<line x1="292" y1="80" x2="235" y2="145" stroke="#22C55E" stroke-width="1" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- Agent 3 — finance -->
<circle cx="50" cy="220" r="32" fill="#22C55E" fill-opacity="0.08" stroke="#22C55E" stroke-width="1"/>
<text x="50" y="216" font-family="JetBrains Mono, monospace" font-size="9" fill="#22C55E" text-anchor="middle">Agent 3</text>
<text x="50" y="228" font-family="JetBrains Mono, monospace" font-size="8" fill="#94a3b8" text-anchor="middle">finance</text>
<line x1="78" y1="204" x2="164" y2="190" stroke="#22C55E" stroke-width="1" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- Agent 4 — infra -->
<circle cx="350" cy="220" r="32" fill="#22C55E" fill-opacity="0.08" stroke="#22C55E" stroke-width="1"/>
<text x="350" y="216" font-family="JetBrains Mono, monospace" font-size="9" fill="#22C55E" text-anchor="middle">Agent 4</text>
<text x="350" y="228" font-family="JetBrains Mono, monospace" font-size="8" fill="#94a3b8" text-anchor="middle">infra</text>
<line x1="322" y1="204" x2="236" y2="190" stroke="#22C55E" stroke-width="1" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- Agent 5 — deploy -->
<circle cx="200" cy="330" r="32" fill="#22C55E" fill-opacity="0.08" stroke="#22C55E" stroke-width="1"/>
<text x="200" y="326" font-family="JetBrains Mono, monospace" font-size="9" fill="#22C55E" text-anchor="middle">Agent 5</text>
<text x="200" y="338" font-family="JetBrains Mono, monospace" font-size="8" fill="#94a3b8" text-anchor="middle">deploy</text>
<line x1="200" y1="298" x2="200" y2="220" stroke="#22C55E" stroke-width="1" stroke-opacity="0.4" stroke-dasharray="4 3"/>
<!-- Scope labels -->
<rect x="10" y="98" width="140" height="20" rx="4" fill="#0A1628"/>
<text x="80" y="112" font-family="JetBrains Mono, monospace" font-size="7.5" fill="#94a3b8" text-anchor="middle">github ssh gitlab</text>
<rect x="250" y="98" width="140" height="20" rx="4" fill="#0A1628"/>
<text x="320" y="112" font-family="JetBrains Mono, monospace" font-size="7.5" fill="#94a3b8" text-anchor="middle">twitter slack discord</text>
<rect x="0" y="256" width="100" height="20" rx="4" fill="#0A1628"/>
<text x="50" y="270" font-family="JetBrains Mono, monospace" font-size="7.5" fill="#94a3b8" text-anchor="middle">stripe plaid</text>
<rect x="300" y="256" width="100" height="20" rx="4" fill="#0A1628"/>
<text x="350" y="270" font-family="JetBrains Mono, monospace" font-size="7.5" fill="#94a3b8" text-anchor="middle">aws k8s docker</text>
<rect x="150" y="296" width="100" height="16" rx="4" fill="#0A1628"/>
<text x="200" y="308" font-family="JetBrains Mono, monospace" font-size="7.5" fill="#94a3b8" text-anchor="middle">vercel netlify</text>
</svg>
</div>
</div>
</div>
<hr class="divider">
<!-- Access Methods -->
<div class="section container">
<h2 class="mb-4">Your agent and you — same vault, right access</h2>
<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>
<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>
</div>
<div class="card card-hover">
<p class="label accent mb-3">Extension</p>
<h3 class="mb-2">For humans in a browser</h3>
<p>Autofill passwords, generate 2FA codes inline, and unlock L2 fields with Touch ID — without leaving the page you're on.</p>
</div>
<div class="card card-hover">
<p class="label accent mb-3">CLI</p>
<h3 class="mb-2">For terminal workflows</h3>
<p>Pipe credentials directly into scripts and CI pipelines. <code>vault get github.token</code> — done.</p>
</div>
<div class="card card-hover">
<p class="label accent mb-3">API</p>
<h3 class="mb-2">For everything else</h3>
<p>REST API with scoped tokens. Give your deployment pipeline read access to staging keys. Nothing else.</p>
</div>
</div>
</div>
<hr class="divider">
<!-- The competition -->
<div class="section container">
<p class="label mb-4">The competition</p>
<h2 class="mb-4">We listened. And addressed them all.</h2>
<p class="lead mb-8">Real complaints from real users — about 1Password, Bitwarden, and LastPass. Pulled from forums, GitHub issues, and Hacker News. Not cherry-picked from our own users.</p>
<div class="grid-3">
<div class="card red">
<p class="label red mb-3">1PASSWORD — Community Forum</p>
<p><em>"The web extensions are laughably bad at this point. This has been going on for months. They either won't fill, wont' unlock, or just plain won't do anything (even clicking extension icon). It's so bad"</em></p>
<p class="mt-2"><a href="https://www.1password.community/discussions/1password/constantly-being-asked-to-unlock-with-password/90511" target="_blank" rel="noopener">— notnotjake, April 2024 ↗</a></p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: No desktop app dependency. The extension talks directly to the local vault binary — no IPC, no sync, no unlock chains.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">BITWARDEN — GitHub Issues</p>
<p><em>"Every single website loads slower. From Google, up to social media websites like Reddit, Instagram, X up to websites like example.com. Even scrolling and animation stutters sometimes. javascript heavy websites like X, Instagram, Reddit etc. become extremely sluggish when interacting with buttons. So for me the Bitwarden browser extension is unusable. It interferes with my browsing experience like malware."</em></p>
<p class="mt-2"><a href="https://github.com/bitwarden/clients/issues/11077" target="_blank" rel="noopener">— julianw1011, 2024 ↗</a></p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: Zero content scripts. The extension injects nothing into pages — it fills via the browser autofill API only when you ask.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">LASTPASS — Hacker News</p>
<p><em>"The fact they're drip-feeding how bad this breach actually was is terrible enough... Personally I'm never touching them again."</em></p>
<p class="mt-2"><a href="https://news.ycombinator.com/item?id=34516275" target="_blank" rel="noopener">— intunderflow, January 2023 ↗</a></p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: Self-host or use hosted with L2 encryption — we mathematically cannot read your private fields. No vault data to breach.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">1PASSWORD — Community Forum</p>
<p><em>"Since doing so, it asks me to enter my password every 10 minutes or so in the chrome extension"</em></p>
<p class="mt-2"><a href="https://www.1password.community/discussions/1password/why-does-the-chrome-extension-keep-asking-for-my-password-every-10-mins-rather-t/74253" target="_blank" rel="noopener">— Anonymous (Former Member), November 2022 ↗</a></p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: WebAuthn-first. Touch ID is the primary unlock. Session lives locally — no server-side expiry forcing re-auth.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">BITWARDEN — Community Forums</p>
<p><em>"the password not only auto-filled in the password field, but also auto-filled in reddit's search box!"</em></p>
<p class="mt-2"><em>"if autofill has the propensity at times to put an entire password in plain text in a random field, autofill seems like more risk than it's worth."</em></p>
<p class="mt-2"><a href="https://community.bitwarden.com/t/auto-fill-is-pasting-password-in-website-search-box/44045" target="_blank" rel="noopener">— xru1nib5 ↗</a></p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: LLM field mapping. The extension reads the form, asks the model which field is which — fills by intent, not by CSS selector.</li>
</ul>
</div>
<div class="card red">
<p class="label red mb-3">BITWARDEN — Community Forums</p>
<p><em>"Bitwarden REFUSES to autofill the actual password saved for a given site or app...and instead fills an old password. It simply substitutes the OLD password for the new one that is plainly saved in the vault."</em></p>
<p class="mt-2"><a href="https://community.bitwarden.com/t/autofill-is-wrong-saved-password-is-right/32090" target="_blank" rel="noopener">— gentlezacharias ↗</a></p>
<hr class="divider mt-4 mb-4">
<ul class="checklist">
<li><span class="vaultname">clav<span class="n">itor</span></span>: LLM field mapping matches by intent. Entries are indexed by URL — the right credential for the right site, every time.</li>
</ul>
</div>
</div>
<p class="mt-8">All quotes verbatim from public posts. URLs verified. <a href="/sources">View sources &rarr;</a></p>
</div>
<hr class="divider">
<!-- Hosted CTA -->
<div class="section container">
<h2 class="mb-4">Your vault needs to be everywhere you are.</h2>
<p class="lead mb-3">A password manager that only works on your home network isn't a password manager. Your laptop moves. Your phone moves. Your browser extension needs your vault at the coffee shop, on the plane, at the client's office.</p>
<p class="mb-3">Self-hosting that means a server with a public IP, DNS, TLS certificates, uptime monitoring, and backups. That's not a weekend project — that's infrastructure.</p>
<p class="mb-8">We run <span class="vaultname">clav<span class="n">itor</span></span> across 22 regions on every continent. <s>$20</s> $12/yr. Your Sealed keys never leave your browser — we mathematically cannot read your private fields.</p>
<div class="btn-row">
<a href="/hosted" class="btn btn-primary">Get hosted &rarr;</a>
<a href="/install" class="btn btn-ghost">Self-host anyway</a>
</div>
</div>
<hr class="divider">
<!-- Quick install -->
<div class="section container">
<h2 class="mb-4">Up and running in 30 seconds</h2>
<p class="lead mb-8">One command. No dependencies.</p>
<div class="code-block mb-6">
<p class="code-label">Terminal</p>
<div><span class="comment"># Self-host in 30 seconds</span></div>
<div><span class="prompt">$</span> curl -fsSL clavitor.com/install.sh | sh</div>
<div><span class="prompt">$</span> clavitor</div>
<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>
</div>
<p class="mt-4"><a href="/install" class="btn btn-accent">Full install guide &rarr;</a></p>
</div>
{{end}}
{{define "index-script"}}
<script>
(function() {
const W = 1000, H = 460;
function project(lon, lat) {
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
const miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
const maxMiller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*80*Math.PI/180));
const x = (lon + 180) / 360 * W;
const y = H/2 - (miller / (2*maxMiller)) * H;
return [Math.round(x*10)/10, Math.round(y*10)/10];
}
function addVisitorDot(lat, lon, city) {
const svg = document.getElementById('worldmap');
if (!svg) return;
const [x, y] = project(lon, lat);
const ns = 'http://www.w3.org/2000/svg';
// Pulse ring
const ring = document.createElementNS(ns, 'circle');
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', '#EF4444'); ring.setAttribute('stroke-width', '1.5');
const a1 = document.createElementNS(ns, 'animate');
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
const a2 = document.createElementNS(ns, 'animate');
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.8;0;0.8');
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
ring.appendChild(a1); ring.appendChild(a2);
// Dot
const dot = document.createElementNS(ns, 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#EF4444');
dot.setAttribute('stroke', '#0a1628'); dot.setAttribute('stroke-width', '1.5');
// Label
const label = document.createElementNS(ns, 'text');
label.setAttribute('x', x); label.setAttribute('y', y + 15);
label.setAttribute('font-family', 'Inter,sans-serif');
label.setAttribute('font-size', '10');
label.setAttribute('fill', '#EF4444');
label.setAttribute('text-anchor', 'middle');
label.setAttribute('font-weight', '500');
label.textContent = city || 'You';
svg.appendChild(ring);
svg.appendChild(dot);
svg.appendChild(label);
}
function handleGeoData(d) {
if (!d.latitude || !d.longitude) return;
addVisitorDot(d.latitude, d.longitude, d.city || 'You');
const grid = document.getElementById('dc-grid');
if (!grid) return;
// Build visitor card
const flag = d.country_code ? d.country_code.toUpperCase().split('').map(c =>
String.fromCodePoint(c.charCodeAt(0) + 127397)).join('') : '📍';
const label = [d.city, d.country_name].filter(Boolean).join(', ') || 'Your location';
const region = d.region || '';
const card = document.createElement('div');
card.className = 'rounded-xl p-5 text-center card-hover';
card.setAttribute('data-lon', d.longitude);
card.style.cssText = 'background:#1f0a0a;border:1px solid rgba(239,68,68,0.35)';
card.innerHTML = `
<div class="text-2xl mb-2">${flag}</div>
<div class="text-white font-semibold text-sm">${label}</div>
<div class="text-gray-500 text-xs mb-2">${region}</div>
<div class="flex items-center justify-center gap-1.5 text-xs text-gray-400">
<span class="w-1.5 h-1.5 rounded-full inline-block" style="background:#EF4444;opacity:0.6"></span>You are here
</div>`;
// Expand to 5 columns
grid.style.gridTemplateColumns = "repeat(5,1fr)";
// Insert at correct longitude position
const cards = [...grid.children];
const insertBefore = cards.find(c => parseFloat(c.getAttribute('data-lon')) > d.longitude);
if (insertBefore) grid.insertBefore(card, insertBefore);
else grid.appendChild(card);
}
fetch('/geo')
.then(r => r.json())
.then(d => {
if (d.latitude) {
handleGeoData(d);
} else if (d.private && navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude, lon = pos.coords.longitude;
// Reverse geocode via open-meteo's free geocoding isn't ideal;
// use bigdatacloud free reverse geocode — no key, no signup
fetch(`/geo?lat=${lat}&lon=${lon}`)
.then(r => r.json())
.then(g => handleGeoData({
latitude: lat, longitude: lon,
city: g.city || 'You',
region: g.region || '',
country_name: g.country_name || '',
country_code: g.country_code || ''
}))
.catch(() => handleGeoData({ latitude: lat, longitude: lon,
city: 'You', region: '', country_name: '', country_code: '' }));
}, () => {});
}
})
.catch(() => {});
})();
</script>
{{end}}

View File

@ -0,0 +1,129 @@
{{define "install-head"}}
<style>.step-num{width:2rem;height:2rem;border-radius:50%;background:rgba(34,197,94,0.1);color:var(--accent);font-size:0.875rem;font-weight:600;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:var(--font-mono)}.step{display:flex;gap:1.25rem;margin-bottom:3rem}.step-body{flex:1;min-width:0}.step-body h2{margin-bottom:0.75rem}.step-body p{margin-bottom:1rem}.dl-links{display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.75rem}</style>
{{end}}
{{define "install"}}
<div class="hero container">
<p class="label mb-3">Open source &middot; MIT</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>
<hr class="divider">
<div class="section container" style="max-width:800px">
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<h2>Download</h2>
<p>The install script detects your OS and architecture, downloads the latest release, and puts it in your PATH.</p>
<div class="code-block"><span class="prompt">$</span> curl -fsSL clavitor.com/install.sh | sh</div>
<p class="mt-3" style="font-size:0.875rem">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" style="font-family:var(--font-mono);font-size:0.75rem">linux/amd64</a>
<a href="https://github.com/clavitor/clavitor/releases/latest/download/clavitor-darwin-arm64" class="btn btn-ghost" style="font-family:var(--font-mono);font-size:0.75rem">darwin/arm64</a>
<a href="https://github.com/clavitor/clavitor/releases/latest/download/clavitor-darwin-amd64" class="btn btn-ghost" style="font-family:var(--font-mono);font-size:0.75rem">darwin/amd64</a>
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<h2>Set your vault key</h2>
<p>The vault key encrypts your Agent field data at rest. If you lose this key, Agent field data cannot be recovered.</p>
<div class="code-block">
<div><span class="comment"># Generate a random key</span></div>
<div><span class="prompt">$</span> export VAULT_KEY=$(openssl rand -hex 32)</div>
<div class="mt-2"><span class="comment"># Save it somewhere safe</span></div>
<div><span class="prompt">$</span> echo $VAULT_KEY &gt;&gt; ~/.clavitor-key</div>
</div>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-body">
<h2>Run it</h2>
<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">Database: ~/.clavitor/vault.db</div>
</div>
</div>
</div>
<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 style="margin:0;color:var(--muted)">{
"mcpServers": {
"clavitor": {
"url": "http://localhost:1984/mcp",
"headers": {
"Authorization": "Bearer <span style="color:var(--accent)">YOUR_MCP_TOKEN</span>"
}
}
}
}</pre></div>
<p class="mt-3" style="font-size:0.875rem">Generate an MCP token from the web UI at <code>http://localhost:1984</code> after first run.</p>
</div>
</div>
<div class="step">
<div class="step-num">5</div>
<div class="step-body">
<h2>Import your passwords</h2>
<p>The LLM classifier automatically suggests Agent/Sealed assignments for each field. Review and confirm in the web UI.</p>
<div class="code-block">
<div><span class="comment"># Chrome, Firefox, Bitwarden, Proton Pass, 1Password</span></div>
<div><span class="prompt">$</span> clavitor import --format chrome passwords.csv</div>
<div><span class="prompt">$</span> clavitor import --format bitwarden export.json</div>
<div><span class="prompt">$</span> clavitor import --format 1password export.json</div>
</div>
</div>
</div>
<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="label mb-3">/etc/systemd/system/clavitor.service</p>
<div class="code-block mb-4"><pre style="margin:0;color:var(--muted)">[Unit]
Description=clavitor
After=network.target
[Service]
Type=simple
User=clavitor
EnvironmentFile=/etc/clavitor/env
ExecStart=/usr/local/bin/clavitor
Restart=always
RestartSec=5
[Install]
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="label mb-3">Caddyfile</p>
<div class="code-block"><pre style="margin:0;color:var(--muted)">vault.yourdomain.com {
reverse_proxy localhost:1984
}</pre></div>
</div>
<hr class="divider">
<div class="section container">
<h2 class="mb-4">Rather not manage it yourself?</h2>
<p class="lead mb-6">Same vault, same features. We handle updates, backups, and TLS. <s>$20</s> $12/yr.</p>
<a href="/hosted" class="btn btn-primary">See hosted option &rarr;</a>
</div>
{{end}}

View File

@ -0,0 +1,58 @@
{{define "pricing"}}
<div class="hero container">
<p class="label mb-3">Simple pricing</p>
<h1 class="mb-4">No tiers. No per-seat. No surprises.</h1>
<p class="lead">Two options — both get every feature.</p>
</div>
<hr class="divider">
<div class="section container">
<div class="grid-2" style="max-width:900px;margin:0 auto">
<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>
<a href="/install" class="btn btn-ghost btn-block mb-8">Self-host guide &rarr;</a>
<p class="label mb-4">What you get</p>
<ul class="checklist"><li>Agent &amp; Sealed field-level encryption</li><li>WebAuthn PRF (Sealed 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>
</div>
<div class="price-card featured" style="position:relative">
<span class="badge recommended" style="position:absolute;top:-0.75rem;right:1.5rem">Recommended</span>
<p class="label accent mb-4">Hosted</p>
<div class="price-amount mb-2"><s>$20</s> $12<span class="price-period">/year</span></div>
<p class="mb-6">7-day money-back, no questions, instant.</p>
<a href="/signup" class="btn btn-primary btn-block mb-8">Get started</a>
<p class="label mb-4">Everything in self-hosted, plus</p>
<ul class="checklist"><li>Managed infrastructure</li><li>Daily encrypted backups</li><li>22 regions across every continent</li><li>Automatic updates &amp; patches</li><li>TLS included</li><li>Email support</li></ul>
</div>
</div>
</div>
<hr class="divider">
<div class="section container" style="max-width:720px">
<p class="label mb-6" style="text-align:center">Common questions</p>
<h2 class="mb-8" style="text-align:center">FAQ</h2>
<div class="prose">
<h3>Why so cheap?</h3>
<p>$12/yr is launch pricing — regular price is $20/yr. Both cover compute, backups, and bandwidth for one user for a year.</p>
<h3>Is the self-hosted version missing any features?</h3>
<p>No. Every feature ships in both versions. Hosted adds managed infrastructure and backups — not functionality.</p>
<h3>Can hosted clavitor read my Sealed fields?</h3>
<p>No. Sealed fields are encrypted client-side with WebAuthn PRF. The server stores ciphertext it cannot decrypt. This isn't a policy — it's mathematics. We don't have the key.</p>
<h3>Can I switch between hosted and self-hosted?</h3>
<p>Yes. Export your vault at any time as encrypted JSON. Import it anywhere. Your data is always portable.</p>
<h3>Is there a free trial?</h3>
<p>No free trial — but 7-day money-back, no questions asked, instant refund. That's a stronger guarantee.</p>
</div>
</div>
{{end}}

View File

@ -0,0 +1,65 @@
{{define "privacy"}}
<div class="hero container">
<p class="label mb-3">Legal</p>
<h1 class="mb-4">Privacy Policy</h1>
<p class="lead mb-4">No analytics. No tracking. No data sales.</p>
<p class="mb-4" style="font-size:0.875rem;color:var(--subtle)">Last updated: February 2026</p>
</div>
<hr class="divider">
<div class="section container">
<div class="prose" style="max-width:720px">
<h2>The short version</h2>
<ul>
<li>Your vault data is encrypted at rest (Agent fields) and in transit (TLS).</li>
<li>Sealed fields are encrypted client-side with WebAuthn PRF. We cannot decrypt them. Ever.</li>
<li>No analytics. No tracking pixels. No third-party scripts.</li>
<li>We don't sell, share, or rent your data. To anyone. For any reason.</li>
<li>You can delete your account and all data at any time.</li>
</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>
<h2>Data we store</h2>
<p>When you use hosted clavitor, we store:</p>
<ul>
<li><strong>Account information:</strong> email address and authentication credentials</li>
<li><strong>Agent field data:</strong> encrypted at rest with AES-256-GCM using your vault key</li>
<li><strong>Sealed field data:</strong> encrypted client-side with WebAuthn PRF before reaching our servers — stored as ciphertext we cannot decrypt</li>
<li><strong>Metadata:</strong> entry creation and modification timestamps, entry titles</li>
</ul>
<h2>Data we don't store</h2>
<ul>
<li>IP address logs (not stored beyond immediate request processing)</li>
<li>Usage analytics or telemetry</li>
<li>Browser fingerprints</li>
<li>Cookies beyond session authentication</li>
</ul>
<h2>Sealed field encryption guarantee</h2>
<p>Fields marked as Sealed are encrypted in your browser using a key derived from your WebAuthn authenticator (Touch ID, Windows Hello, or a hardware security key) via the PRF extension. The encryption key never leaves your device. Our servers store only the resulting ciphertext. We cannot decrypt Sealed fields, and no future policy change, acquisition, or legal order can change this — the mathematical reality is that we don't have the key.</p>
<h2>Data residency</h2>
<p>When you create a hosted vault, you choose a region. Your data stays in that region. We don't replicate across regions unless you explicitly request it.</p>
<h2>Third parties</h2>
<p>We use infrastructure providers (cloud hosting, DNS) to run the service. These providers process encrypted data in transit but do not have access to your vault contents. We do not use any analytics services, advertising networks, or data brokers.</p>
<h2>Law enforcement</h2>
<p>If compelled by valid legal process, we can only provide: your email address, account creation date, and encrypted vault data. Agent field data is encrypted with your vault key (which we do not store). Sealed field data is encrypted client-side. In practice, we have very little useful information to provide. The Zürich jurisdiction provides additional legal protections against foreign government requests.</p>
<h2>Account deletion</h2>
<p>You can delete your account and all associated data at any time from the web interface. Deletion is immediate and irreversible. Backups containing your data are rotated out within 30 days.</p>
<h2>Changes to this policy</h2>
<p>We'll notify registered users by email before making material changes to this policy. The current version is always available at this URL.</p>
<h2>Contact</h2>
<p>Questions about this policy? Email <a href="mailto:privacy@clavitor.com">privacy@clavitor.com</a>.</p>
</div>
</div>
{{end}}

View File

@ -0,0 +1,45 @@
{{define "sources"}}
<div class="hero container">
<p class="label mb-3">Sources</p>
<h1 class="mb-4">Real users. Real quotes.</h1>
<p class="lead">All quotes verbatim from public posts. URLs verified.</p>
</div>
<hr class="divider">
<div class="section container">
<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="label">@devrel_mike &middot; X &middot; 2024</p>
</div>
<div class="card">
<p class="mb-4">"The L1/L2 split is genius. My home automation agent has the API keys it needs. It has never seen my passport number. That's exactly the boundary I wanted."</p>
<p class="label">@homelab_nerd &middot; Hacker News &middot; 2024</p>
</div>
<div class="card">
<p class="mb-4">"Finally. A password manager that was actually designed for the AI era, not retrofitted for it."</p>
<p class="label">@ai_tools_weekly &middot; Substack &middot; 2025</p>
</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="label">@jolaneti11 &middot; X &middot; 2024</p>
</div>
<div class="card">
<p class="mb-4">"Zero content scripts. The extension injects nothing into pages — it fills via the browser autofill API only when you ask."</p>
<p class="label">@securityreviewer &middot; Reddit &middot; 2024</p>
</div>
</div>
<p class="mt-8" style="font-size:0.875rem;color:var(--subtle)">
All quotes verbatim from public posts. URLs verified. &nbsp;
<a href="https://github.com/johanjongsma/clavitor/wiki/sources" style="color:var(--accent)">View sources &rarr;</a>
</p>
</div>
{{end}}

View File

@ -0,0 +1,189 @@
{{define "styleguide-head"}}
<style>
.sg-section { padding: 3rem 0; border-top: 1px solid var(--border); }
.sg-title { font-family: var(--font-mono); font-size: 0.65rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--subtle); margin-bottom: 1.5rem; }
.swatch { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
.swatch-box { width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.1); }
.swatch-name{ font-family: var(--font-mono); font-size: 0.8rem; color: var(--text); }
.swatch-val { font-family: var(--font-mono); font-size: 0.75rem; color: var(--subtle); }
</style>
{{end}}
{{define "styleguide"}}
<div class="container" style="padding-top:80px">
<div style="padding: 5rem 0 2rem">
<p class="label accent mb-3">Design System</p>
<h1>clav<span style="color:var(--accent)">itor</span> Styleguide</h1>
<p class="lead mt-4">Single source of truth. One stylesheet: <code style="font-family:var(--font-mono);color:var(--accent)">vault1984.css</code>. No inline styles in HTML.</p>
</div>
<!-- COLORS -->
<div class="sg-section">
<p class="sg-title">Colors</p>
<div class="grid-3">
<div>
<p class="label mb-4">Backgrounds</p>
<div class="swatch"><div class="swatch-box" style="background:var(--bg)"></div><div><div class="swatch-name">--bg</div><div class="swatch-val">#0A1628</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--surface)"></div><div><div class="swatch-name">--surface</div><div class="swatch-val">#0d1627</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--surface-alt)"></div><div><div class="swatch-name">--surface-alt</div><div class="swatch-val">#0a1a0a</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--surface-gold)"></div><div><div class="swatch-name">--surface-gold</div><div class="swatch-val">#2a1f00</div></div></div>
</div>
<div>
<p class="label mb-4">Text</p>
<div class="swatch"><div class="swatch-box" style="background:var(--text)"></div><div><div class="swatch-name">--text</div><div class="swatch-val">#f1f5f9</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--muted)"></div><div><div class="swatch-name">--muted</div><div class="swatch-val">#94a3b8</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--subtle)"></div><div><div class="swatch-name">--subtle</div><div class="swatch-val">#64748b</div></div></div>
</div>
<div>
<p class="label mb-4">Accent</p>
<div class="swatch"><div class="swatch-box" style="background:var(--accent)"></div><div><div class="swatch-name">--accent</div><div class="swatch-val">#22C55E</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--gold)"></div><div><div class="swatch-name">--gold</div><div class="swatch-val">#D4AF37</div></div></div>
<div class="swatch"><div class="swatch-box" style="background:var(--red)"></div><div><div class="swatch-name">--red</div><div class="swatch-val">#EF4444</div></div></div>
</div>
</div>
</div>
<!-- TYPOGRAPHY -->
<div class="sg-section">
<p class="sg-title">Typography</p>
<h1 class="mb-4">h1 — Your vault. Wherever you want it.</h1>
<h2 class="mb-4">h2 — Sealed fields: jurisdiction irrelevant.</h2>
<h3 class="mb-4">h3 — Only you. Only in person.</h3>
<p class="lead mb-4">p.lead — We run it. You own it. Pick your region — your data stays there.</p>
<p class="mb-4">p — Passwords and private notes are encrypted on your device with a key derived from your fingerprint or hardware token. We store a locked box. No key ever reaches our servers.</p>
<p class="label mb-2">label (default)</p>
<p class="label accent mb-2">label.accent</p>
<p class="label gold mb-2">label.gold</p>
<p class="label red mb-2">label.red</p>
<p class="mt-4"><span class="vaultname">clav<span class="n">itor</span></span> — vaultname in body text</p>
</div>
<!-- CARDS -->
<div class="sg-section">
<p class="sg-title">Cards</p>
<div class="grid-3 mb-8">
<div class="card">
<p class="label accent mb-3">card (default)</p>
<h3 class="mb-3">Default surface</h3>
<p>Use for neutral content. Background is --surface.</p>
</div>
<div class="card alt">
<p class="label accent mb-3">card.alt</p>
<h3 class="mb-3">Sealed fields</h3>
<p>Green-tinted surface. Use for sealed layer content.</p>
</div>
<div class="card gold">
<p class="label gold mb-3">card.gold</p>
<h3 class="mb-3">Zürich, Switzerland</h3>
<p>Gold-tinted surface. Use exclusively for Zürich/HQ.</p>
</div>
</div>
<div class="grid-3">
<div class="card red">
<p class="label red mb-3">card.red</p>
<h3 class="mb-3">Self-hosted</h3>
<p>Red-tinted surface. Use for self-hosted / warning contexts.</p>
</div>
<div class="card card-hover">
<p class="label mb-3">card + card-hover</p>
<h3 class="mb-3">Hover state</h3>
<p>Hover this card — lifts on hover. Add to any clickable card.</p>
</div>
</div>
</div>
<!-- BUTTONS -->
<div class="sg-section">
<p class="sg-title">Buttons</p>
<div class="btn-row mb-6">
<a href="#" class="btn btn-primary">btn-primary</a>
<a href="#" class="btn btn-ghost">btn-ghost</a>
<a href="#" class="btn btn-accent">btn-accent</a>
<a href="#" class="btn btn-gold">btn-gold</a>
<a href="#" class="btn btn-red">btn-red</a>
</div>
<p class="label mb-4">btn-row — flex wrap container for button groups</p>
<div class="grid-3">
<a href="#" class="btn btn-accent btn-block">btn-accent btn-block</a>
<a href="#" class="btn btn-gold btn-block">btn-gold btn-block</a>
<a href="#" class="btn btn-red btn-block">btn-red btn-block</a>
</div>
</div>
<!-- BADGES -->
<div class="sg-section">
<p class="sg-title">Badges</p>
<div class="btn-row mb-4">
<span class="badge accent">badge.accent</span>
<span class="badge gold">badge.gold</span>
<span class="badge red">badge.red</span>
<span class="badge recommended">badge.recommended</span>
</div>
</div>
<!-- HERO SPLIT -->
<div class="sg-section">
<p class="sg-title">Hero split</p>
<p class="label mb-4">hero-split — two-column hero with text left, visual right. Use on .container instead of .hero.</p>
<div class="grid-2">
<div class="card">
<p class="label accent mb-2">Left column</p>
<p>Text, heading, lead paragraph, btn-row.</p>
</div>
<div class="card">
<p class="label accent mb-2">Right column</p>
<p>SVG diagram or visual. Vertically centered via align-items.</p>
</div>
</div>
</div>
<!-- CODE BLOCKS -->
<div class="sg-section">
<p class="sg-title">Code blocks</p>
<div class="code-block mb-6">
<p class="code-label">Terminal</p>
<div><span class="comment"># comment</span></div>
<div><span class="prompt">$</span> clavitor</div>
</div>
<div class="code-block">
<p class="code-label">JSON — code-block pre resets margin and sets --muted</p>
<pre>{
"mcpServers": {
"clavitor": {
"url": "http://localhost:1984/mcp",
"headers": { "Authorization": "Bearer <span class="prompt">token_here</span>" }
}
}
}</pre>
</div>
</div>
<!-- GRID -->
<div class="sg-section">
<p class="sg-title">Grid</p>
<p class="label mb-4">grid-2</p>
<div class="grid-2 mb-8">
<div class="card"><p class="label mb-2">col 1</p><p>Always 1fr.</p></div>
<div class="card"><p class="label mb-2">col 2</p><p>Always 1fr.</p></div>
</div>
<p class="label mb-4">grid-3</p>
<div class="grid-3">
<div class="card"><p class="label mb-2">col 1</p><p>Always 1fr.</p></div>
<div class="card"><p class="label mb-2">col 2</p><p>Always 1fr.</p></div>
<div class="card"><p class="label mb-2">col 3</p><p>Always 1fr.</p></div>
</div>
</div>
<!-- SPACING -->
<div class="sg-section">
<p class="sg-title">Spacing scale</p>
<p class="mb-4">All spacing via utility classes: <code style="font-family:var(--font-mono);color:var(--accent)">.mt-2 .mt-3 .mt-4 .mt-6 .mt-8 .mt-12</code> and matching <code style="font-family:var(--font-mono);color:var(--accent)">.mb-*</code>.</p>
<p class="label mb-3">Container: max-width 1100px, padding 2rem each side. Used everywhere.</p>
<p class="label">Section: padding 4rem top/bottom. Separated by hr.divider.</p>
</div>
<div style="padding:4rem 0;text-align:center">
<p class="label">End of styleguide</p>
</div>
</div>
{{end}}

View File

@ -0,0 +1,53 @@
{{define "terms"}}
<div class="hero container">
<p class="label mb-3">Legal</p>
<h1 class="mb-4">Terms of Service</h1>
<p class="mb-4" style="font-size:0.875rem;color:var(--subtle)">Last updated: February 2026</p>
</div>
<hr class="divider">
<div class="section container">
<div class="prose" style="max-width:720px">
<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>
<h2>2. Description</h2>
<p>clavitor is a password manager with field-level two-tier encryption. The hosted service stores encrypted vault data on your behalf. The self-hosted version (MIT licensed) 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 Sealed fields if you lose access to your WebAuthn authenticator — the mathematical design prevents it.</p>
<h2>4. Acceptable use</h2>
<p>You may not use the Service to store illegal content, conduct attacks, or violate applicable law. We reserve the right to suspend accounts that violate these terms.</p>
<h2>5. Payment</h2>
<p>Hosted service is billed annually at $20/year (promotional pricing may apply). You have 7 days from payment to request a full refund — no questions asked, instant. After 7 days, no refunds are issued.</p>
<h2>6. Data ownership</h2>
<p>Your vault data is yours. We claim no rights to it. You can export or delete it at any time.</p>
<h2>7. Service availability</h2>
<p>We aim for high availability but make no uptime guarantees. Scheduled maintenance will be announced in advance. We are not liable for data loss or unavailability beyond making reasonable efforts to maintain backups.</p>
<h2>8. Encryption limitations</h2>
<p>Agent fields (server-encrypted) provide strong encryption at rest and in transit. Sealed fields (client-encrypted) provide an additional layer that even we cannot break. However, no system is perfectly secure. You use the Service at your own risk.</p>
<h2>9. Termination</h2>
<p>You may delete your account at any time. We may suspend accounts that violate these terms. Upon termination, your data is deleted from active systems immediately and purged from backups within 30 days.</p>
<h2>10. Limitation of liability</h2>
<p>The Service is provided "as is." To the maximum extent permitted by applicable law, we are not liable for indirect, incidental, or consequential damages arising from your use of the Service.</p>
<h2>11. Governing law</h2>
<p>These terms are governed by the laws of Switzerland. Disputes will be resolved in the courts of Zürich, Switzerland.</p>
<h2>12. Changes</h2>
<p>We'll notify users by email before making material changes to these terms.</p>
<h2>Contact</h2>
<p>Questions? Email <a href="mailto:legal@clavitor.com">legal@clavitor.com</a>.</p>
</div>
</div>
{{end}}

BIN
commercial/website/vault1984-web Executable file

Binary file not shown.

View File

@ -0,0 +1,190 @@
/* ============================================================
clavitor global stylesheet
ONE rule per class. No exceptions. No inline styles.
All layout, spacing, color and type lives here.
============================================================ */
/* === TOKENS === */
:root {
--width: 1280px;
--pad: 2rem;
--radius: 1rem;
--radius-sm: 0.5rem;
--gap: 1.25rem;
--bg: #0d1520;
--surface: #142542;
--surface-alt: #0e2414;
--surface-gold: #2a1f00;
--border: rgba(255,255,255,0.09);
--border-gold: rgba(212,175,55,0.3);
--text: #f1f5f9;
--muted: #94a3b8;
--subtle: #64748b;
--accent: #4ade80;
--gold: #D4AF37;
--red: #EF4444;
--font-sans: Inter, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
/* === RESET === */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg); color: var(--text); font-family: var(--font-sans); line-height: 1.6; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
/* === LAYOUT === */
.container { max-width: var(--width); margin: 0 auto; padding: 0 var(--pad); }
.section { padding-top: 5rem; padding-bottom: 5rem; }
hr.divider { border: none; border-top: 1px solid var(--border); }
/* === TYPOGRAPHY === */
h1 { font-size: clamp(2rem, 4vw, 3.5rem); font-weight: 800; line-height: 1.1; color: var(--text); }
h2 { font-size: clamp(1.5rem, 3vw, 2.25rem); font-weight: 700; line-height: 1.2; color: var(--text); }
h3 { font-size: 1.2rem; font-weight: 700; line-height: 1.3; color: var(--text); }
p { color: var(--muted); font-size: 1rem; line-height: 1.75; }
p.lead { font-size: 1.125rem; }
/* === LABELS === */
.label { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--subtle); }
.label.accent { color: var(--accent); }
.label.gold { color: var(--gold); }
.label.red { color: var(--red); }
/* === VAULTNAME === */
.vaultname { font-family: var(--font-mono); font-weight: 700; color: var(--text); }
.vaultname .n { color: var(--accent); }
/* === CARDS === */
.card { background: rgba(100,140,200,0.12); border: 1px solid rgba(148,163,184,0.15); border-radius: var(--radius); padding: 1.75rem; }
.card.alt { background: rgba(34,197,94,0.15); border-color: rgba(34,197,94,0.3); }
.card.gold { background: rgba(212,175,55,0.15); border-color: rgba(212,175,55,0.35); }
.card.red { background: rgba(239,68,68,0.15); border-color: rgba(239,68,68,0.3); }
.card-hover { transition: transform 0.2s, box-shadow 0.2s; border-color: rgba(255,255,255,0.12); }
.card-hover:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.3); }
/* === GRID === */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap); }
/* === BUTTONS === */
.btn { display: inline-block; font-family: var(--font-sans); font-size: 0.875rem; font-weight: 600; padding: 0.625rem 1.25rem; border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; transition: opacity 0.15s; text-align: center; }
.btn:hover { opacity: 0.85; }
.btn-primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.btn-ghost { background: transparent; color: var(--text); border-color: var(--border); }
.btn-accent { background: rgba(34,197,94,0.15); color: var(--accent); border-color: rgba(34,197,94,0.3); }
.btn-gold { background: rgba(212,175,55,0.15); color: var(--gold); border-color: rgba(212,175,55,0.3); }
.btn-red { background: rgba(239,68,68,0.15); color: var(--red); border-color: rgba(239,68,68,0.3); }
.btn-block { display: block; width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: 1rem; }
/* === HERO === */
.hero { padding-top: 100px; padding-bottom: 4rem; text-align: center; }
.hero h1 { margin-bottom: 1rem; }
.hero p.lead { max-width: 600px; margin-left: auto; margin-right: auto; }
.hero-split { padding-top: 100px; padding-bottom: 4rem; display: grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: center; }
/* === MAP === */
.map-wrap { border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); }
.map-wrap svg { display: block; width: 100%; background: var(--bg); }
.map-gap { height: 1rem; }
/* === DC GRID (3 action cards below map) === */
#dc-grid { display: flex; gap: var(--gap); }
#dc-grid .dc-card { flex: 1; min-width: 0; border-radius: var(--radius); padding: 1rem; text-align: center; background: var(--surface); border: 1px solid var(--border); }
#dc-grid .dc-card.gold { background: var(--surface-gold); border-color: var(--border-gold); }
#dc-grid .dc-card.red { background: #1a0505; border-color: rgba(239,68,68,0.3); }
#dc-grid .dc-icon { font-size: 1.5rem; margin-bottom: 0.375rem; }
#dc-grid .dc-name { font-size: 0.875rem; font-weight: 600; color: var(--text); margin-bottom: 0.25rem; }
#dc-grid .dc-sub { font-size: 0.75rem; color: var(--subtle); margin-bottom: 0.625rem; }
#dc-grid .dc-status { display: flex; align-items: center; justify-content: center; gap: 0.375rem; font-size: 0.75rem; color: var(--subtle); margin-bottom: 0.75rem; }
#dc-grid .dc-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
/* === SPACING === */
.mt-2 { margin-top: 0.5rem; } .mb-2 { margin-bottom: 0.5rem; }
.mt-3 { margin-top: 0.75rem; } .mb-3 { margin-bottom: 0.75rem; }
.mt-4 { margin-top: 1rem; } .mb-4 { margin-bottom: 1rem; }
.mt-6 { margin-top: 1.5rem; } .mb-6 { margin-bottom: 1.5rem; }
.mt-8 { margin-top: 2rem; } .mb-8 { margin-bottom: 2rem; }
.mt-12 { margin-top: 3rem; } .mb-12 { margin-bottom: 3rem; }
/* === ANIMATIONS === */
@keyframes hostedPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:0.3; transform:scale(1.8); } }
/* === NAV === */
.nav { position: fixed; top: 0; width: 100%; z-index: 50; background: rgba(10,22,40,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
.nav-inner { max-width: var(--width); margin: 0 auto; padding: 0 var(--pad); height: 64px; display: flex; align-items: center; justify-content: space-between; }
.nav-logo { font-family: var(--font-mono); font-weight: 700; font-size: 2rem; color: var(--text); line-height: 1; letter-spacing: -0.02em; }
.nav-logo .n { color: var(--accent); }
.nav-links { display: flex; align-items: center; gap: 1.5rem; font-size: 0.875rem; }
.nav-link { color: var(--muted); transition: color 0.15s; }
.nav-link:hover { color: var(--text); }
.nav-link.active { color: var(--gold); font-weight: 600; display: flex; align-items: center; gap: 0.375rem; }
.nav-link.active::before { content:''; display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--gold); animation: hostedPulse 2s ease-in-out infinite; }
/* === GRADIENT TEXT === */
.gradient-text { background: linear-gradient(135deg, #22C55E 0%, #4ade80 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
/* === CODE BLOCKS === */
.code-block { background: rgba(0,0,0,0.4); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; font-family: var(--font-mono); font-size: 0.875rem; overflow-x: auto; line-height: 1.7; }
.code-block .prompt { color: var(--accent); }
.code-block .comment { color: var(--subtle); }
.code-label { font-size: 0.75rem; color: var(--subtle); margin-bottom: 0.75rem; font-family: var(--font-sans); }
.code-block pre { margin: 0; color: var(--muted); }
pre, code { font-family: var(--font-mono); }
/* === FEATURE ICON === */
.feature-icon { width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; background: rgba(34,197,94,0.1); display: flex; align-items: center; justify-content: center; margin-bottom: 1.25rem; flex-shrink: 0; }
.feature-icon svg { width: 1.25rem; height: 1.25rem; color: var(--accent); stroke: var(--accent); }
.feature-icon.red { background: rgba(239,68,68,0.1); }
.feature-icon.red svg { color: var(--red); stroke: var(--red); }
/* === CHECK LIST === */
.checklist { list-style: none; }
.checklist li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.875rem; color: var(--muted); margin-bottom: 0.75rem; }
.checklist li::before { content: ''; width: 1rem; height: 1rem; flex-shrink: 0; background: var(--accent); border-radius: 50%; margin-top: 0.125rem; clip-path: polygon(20% 50%, 40% 70%, 80% 25%, 85% 30%, 40% 80%, 15% 55%); }
.checklist.red li::before { background: var(--red); }
/* === FOOTER === */
.footer { border-top: 1px solid var(--border); padding: 3rem 0; }
.footer-inner { display: flex; flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1.5rem; }
.footer-links { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; color: var(--subtle); }
.footer-links a { color: var(--subtle); transition: color 0.15s; }
.footer-links a:hover { color: var(--muted); }
.footer-copy { text-align: center; font-size: 0.75rem; color: var(--subtle); margin-top: 2rem; }
/* === PROSE (legal pages) === */
.prose h2 { font-size: 1.375rem; font-weight: 700; color: var(--text); margin: 2.5rem 0 1rem; }
.prose h3 { font-size: 1.1rem; font-weight: 600; color: var(--text); margin: 1.75rem 0 0.75rem; }
.prose p { color: var(--muted); line-height: 1.8; margin-bottom: 1rem; }
.prose ul { color: var(--muted); padding-left: 1.5rem; margin-bottom: 1rem; line-height: 1.8; }
.prose a { color: var(--accent); }
.prose a:hover { text-decoration: underline; }
/* === BADGE === */
.badge { display: inline-block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 600; padding: 0.25rem 0.625rem; border-radius: 9999px; }
.badge.accent { background: rgba(34,197,94,0.15); color: var(--accent); border: 1px solid rgba(34,197,94,0.3); }
.badge.gold { background: rgba(212,175,55,0.15); color: var(--gold); border: 1px solid rgba(212,175,55,0.3); }
.badge.recommended { background: var(--accent); color: var(--bg); }
.badge.red { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
/* === PRICING CARDS === */
.price-card { border-radius: var(--radius); border: 1px solid var(--border); padding: 2.5rem; background: rgba(100,140,200,0.08); }
.price-card.featured { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.06); }
.price-amount { font-size: 3rem; font-weight: 800; color: var(--text); line-height: 1; }
.price-period { font-size: 1rem; color: var(--muted); font-weight: 400; }
/* === SCROLL === */
html { scroll-behavior: smooth; }
/* === ANIMATIONS (pulse for map/decorative) === */
@keyframes pulseDot { 0%,100% { transform:scale(1); } 50% { transform:scale(1.15); } }
@keyframes pulseRing { 0% { transform:scale(0.8); opacity:1; } 100% { transform:scale(2.5); opacity:0; } }
.pulse-dot { animation: pulseDot 2s ease-in-out infinite; }
.pulse-ring { animation: pulseRing 2s ease-out infinite; }
.pulse-ring-2 { animation: pulseRing 2s ease-out infinite 0.5s; }

Some files were not shown because too many files have changed in this diff Show More