chore: auto-commit uncommitted changes
This commit is contained in:
parent
7c7cfa7a4a
commit
b93a33d577
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -149,7 +149,7 @@ code { font-size: 0.875em; }
|
||||||
/* === CHECKLISTS === */
|
/* === CHECKLISTS === */
|
||||||
.checklist { list-style: none; }
|
.checklist { list-style: none; }
|
||||||
.checklist li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.75rem; }
|
.checklist li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.75rem; }
|
||||||
.checklist li::before { content: ''; width: 1rem; height: 1rem; flex-shrink: 0; background: var(--brand-black); border-radius: 50%; margin-top: 0.125rem; clip-path: polygon(20% 50%, 40% 70%, 80% 25%, 85% 30%, 40% 80%, 15% 55%); }
|
.checklist li::before { content: ''; width: 0.5rem; height: 0.5rem; flex-shrink: 0; background: var(--brand-black); margin-top: 0.375rem; }
|
||||||
.checklist.red li::before { background: var(--brand-red); }
|
.checklist.red li::before { background: var(--brand-red); }
|
||||||
|
|
||||||
/* === BADGES === */
|
/* === BADGES === */
|
||||||
|
|
@ -263,4 +263,70 @@ code { font-size: 0.875em; }
|
||||||
#dc-grid { flex-direction: column; }
|
#dc-grid { flex-direction: column; }
|
||||||
.nav-links { gap: 0.75rem; font-size: 0.75rem; }
|
.nav-links { gap: 0.75rem; font-size: 0.75rem; }
|
||||||
.section { padding-top: 48px; padding-bottom: 48px; }
|
.section { padding-top: 48px; padding-bottom: 48px; }
|
||||||
|
.glass-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Looking Glass ---- */
|
||||||
|
.glass-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.glass-pop {
|
||||||
|
background: var(--bg);
|
||||||
|
padding: 20px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.glass-pop:hover { background: var(--surface); }
|
||||||
|
.glass-live { border-left: 3px solid var(--brand-red); }
|
||||||
|
.glass-planned { border-left: 3px solid var(--border); }
|
||||||
|
.glass-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.glass-city {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.glass-status {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.glass-status-live { background: var(--brand-red); color: #fff; }
|
||||||
|
.glass-status-planned { background: var(--surface); color: var(--muted); }
|
||||||
|
.glass-details { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.glass-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
.glass-key { color: var(--muted); }
|
||||||
|
.glass-val { color: var(--text); }
|
||||||
|
.glass-val.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; }
|
||||||
|
.glass-muted { color: var(--muted); font-style: italic; }
|
||||||
|
.glass-fast { color: #16a34a; font-weight: 600; }
|
||||||
|
.glass-ok { color: #ca8a04; font-weight: 600; }
|
||||||
|
.glass-slow { color: var(--brand-red); font-weight: 600; }
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.glass-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.glass-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.glass-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,3 +1,5 @@
|
||||||
module github.com/clavitor/clavitor-web
|
module github.com/clavitor/clavitor-web
|
||||||
|
|
||||||
go 1.23.6
|
go 1.23.6
|
||||||
|
|
||||||
|
require github.com/mattn/go-sqlite3 v1.14.37 // indirect
|
||||||
|
|
|
||||||
|
|
@ -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=
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/*.tmpl
|
//go:embed templates/*.tmpl
|
||||||
|
|
@ -19,12 +25,27 @@ var static embed.FS
|
||||||
|
|
||||||
var templates *template.Template
|
var templates *template.Template
|
||||||
var devMode bool
|
var devMode bool
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
type Pop struct {
|
||||||
|
PopID int
|
||||||
|
City string
|
||||||
|
Country string
|
||||||
|
Lat float64
|
||||||
|
Lon float64
|
||||||
|
RegionName string
|
||||||
|
IP string
|
||||||
|
DNS string
|
||||||
|
Status string
|
||||||
|
Provider string
|
||||||
|
}
|
||||||
|
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Page string
|
Page string
|
||||||
Title string
|
Title string
|
||||||
Desc string
|
Desc string
|
||||||
ActiveNav string
|
ActiveNav string
|
||||||
|
Pops []Pop
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTemplates() {
|
func loadTemplates() {
|
||||||
|
|
@ -36,6 +57,25 @@ func loadTemplates() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadPops() []Pop {
|
||||||
|
rows, err := db.Query("SELECT pop_id, city, country, lat, lon, region_name, ip, dns, status, provider FROM pops ORDER BY pop_id")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("pops query error: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var pops []Pop
|
||||||
|
for rows.Next() {
|
||||||
|
var p Pop
|
||||||
|
if err := rows.Scan(&p.PopID, &p.City, &p.Country, &p.Lat, &p.Lon, &p.RegionName, &p.IP, &p.DNS, &p.Status, &p.Provider); err != nil {
|
||||||
|
log.Printf("pops scan error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pops = append(pops, p)
|
||||||
|
}
|
||||||
|
return pops
|
||||||
|
}
|
||||||
|
|
||||||
func render(w http.ResponseWriter, data PageData) {
|
func render(w http.ResponseWriter, data PageData) {
|
||||||
if devMode {
|
if devMode {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
|
|
@ -74,6 +114,13 @@ func main() {
|
||||||
}
|
}
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
db, err = sql.Open("sqlite3", "clavitor.db?mode=ro")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open clavitor.db: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "8099"
|
port = "8099"
|
||||||
|
|
@ -81,8 +128,29 @@ func main() {
|
||||||
|
|
||||||
http.HandleFunc("/geo", geoHandler)
|
http.HandleFunc("/geo", geoHandler)
|
||||||
|
|
||||||
|
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
host := r.URL.Query().Get("host")
|
||||||
|
if host == "" {
|
||||||
|
http.Error(w, `{"error":"missing host"}`, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
conn, err := net.DialTimeout("tcp", host+":1984", 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(`{"error":"unreachable"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
ms := time.Since(start).Milliseconds()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintf(w, `{"ms":%d}`, ms)
|
||||||
|
})
|
||||||
|
|
||||||
http.HandleFunc("/hosted", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/hosted", func(w http.ResponseWriter, r *http.Request) {
|
||||||
render(w, PageData{Page: "hosted", Title: "clavitor — Hosted", ActiveNav: "hosted"})
|
data := PageData{Page: "hosted", Title: "clavitor — Hosted", ActiveNav: "hosted"}
|
||||||
|
data.Pops = loadPops()
|
||||||
|
render(w, data)
|
||||||
})
|
})
|
||||||
http.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) {
|
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"})
|
render(w, PageData{Page: "install", Title: "Self-host — clavitor", Desc: "Self-host clavitor in 30 seconds. One binary, no dependencies.", ActiveNav: "install"})
|
||||||
|
|
@ -102,6 +170,11 @@ func main() {
|
||||||
http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) {
|
||||||
render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"})
|
render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"})
|
||||||
})
|
})
|
||||||
|
http.HandleFunc("/glass", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := PageData{Page: "glass", Title: "Looking Glass — clavitor"}
|
||||||
|
data.Pops = loadPops()
|
||||||
|
render(w, data)
|
||||||
|
})
|
||||||
|
|
||||||
// Catch-all: index page at "/" or static files or .html redirects
|
// Catch-all: index page at "/" or static files or .html redirects
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,12 @@
|
||||||
{{else if eq .Page "terms"}}{{template "terms" .}}
|
{{else if eq .Page "terms"}}{{template "terms" .}}
|
||||||
{{else if eq .Page "sources"}}{{template "sources" .}}
|
{{else if eq .Page "sources"}}{{template "sources" .}}
|
||||||
{{else if eq .Page "styleguide"}}{{template "styleguide" .}}
|
{{else if eq .Page "styleguide"}}{{template "styleguide" .}}
|
||||||
|
{{else if eq .Page "glass"}}{{template "glass" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if ne .Page "styleguide"}}{{template "footer"}}{{end}}
|
{{if ne .Page "styleguide"}}{{template "footer"}}{{end}}
|
||||||
{{if eq .Page "index"}}{{template "index-script"}}
|
{{if eq .Page "index"}}{{template "index-script"}}
|
||||||
{{else if eq .Page "hosted"}}{{template "hosted-script"}}
|
{{else if eq .Page "hosted"}}{{template "hosted-script" .}}
|
||||||
|
{{else if eq .Page "glass"}}{{template "glass-script"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
{{define "glass"}}
|
||||||
|
<div class="hero container">
|
||||||
|
<p class="label accent mb-4">Network</p>
|
||||||
|
<h1 class="mb-4">Looking Glass</h1>
|
||||||
|
<p class="lead">{{len .Pops}} points of presence. Real-time status.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="divider">
|
||||||
|
|
||||||
|
<div class="section container">
|
||||||
|
<div class="glass-grid">
|
||||||
|
{{range .Pops}}
|
||||||
|
<div class="glass-pop {{if eq .Status "live"}}glass-live{{else}}glass-planned{{end}}">
|
||||||
|
<div class="glass-header">
|
||||||
|
<span class="glass-city">{{.City}}</span>
|
||||||
|
<span class="glass-status {{if eq .Status "live"}}glass-status-live{{else}}glass-status-planned{{end}}">{{.Status}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="glass-details">
|
||||||
|
<div class="glass-row">
|
||||||
|
<span class="glass-key">Region</span>
|
||||||
|
<span class="glass-val mono">{{.RegionName}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="glass-row">
|
||||||
|
<span class="glass-key">Provider</span>
|
||||||
|
<span class="glass-val">{{.Provider}}</span>
|
||||||
|
</div>
|
||||||
|
{{if .IP}}<div class="glass-row">
|
||||||
|
<span class="glass-key">IPv4</span>
|
||||||
|
<span class="glass-val mono">{{.IP}}</span>
|
||||||
|
</div>{{end}}
|
||||||
|
{{if .DNS}}<div class="glass-row">
|
||||||
|
<span class="glass-key">DNS</span>
|
||||||
|
<span class="glass-val mono">{{.DNS}}</span>
|
||||||
|
</div>{{end}}
|
||||||
|
<div class="glass-row">
|
||||||
|
<span class="glass-key">Latency</span>
|
||||||
|
<span class="glass-val glass-latency" data-dns="{{.DNS}}" data-status="{{.Status}}">{{if eq .Status "live"}}—{{else}}<span class="glass-muted">Q2 2026</span>{{end}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "glass-script"}}
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const grid = document.querySelector('.glass-grid');
|
||||||
|
let pending = 0;
|
||||||
|
const pops = grid.querySelectorAll('.glass-pop');
|
||||||
|
|
||||||
|
document.querySelectorAll('.glass-latency[data-status="live"][data-dns]').forEach(el => {
|
||||||
|
const dns = el.dataset.dns;
|
||||||
|
if (!dns) { el.textContent = '—'; return; }
|
||||||
|
pending++;
|
||||||
|
const t0 = performance.now();
|
||||||
|
const t0 = performance.now();
|
||||||
|
fetch('https://' + dns + ':1984/ping').then(() => {
|
||||||
|
const ms = Math.round(performance.now() - t0);
|
||||||
|
el.textContent = ms + ' ms';
|
||||||
|
el.dataset.ms = ms;
|
||||||
|
el.classList.add(ms < 100 ? 'glass-fast' : ms < 250 ? 'glass-ok' : 'glass-slow');
|
||||||
|
if (--pending === 0) sortGrid();
|
||||||
|
}).catch(() => {
|
||||||
|
const ms = Math.round(performance.now() - t0);
|
||||||
|
if (ms < 4900) {
|
||||||
|
el.textContent = ms + ' ms';
|
||||||
|
el.dataset.ms = ms;
|
||||||
|
el.classList.add(ms < 100 ? 'glass-fast' : ms < 250 ? 'glass-ok' : 'glass-slow');
|
||||||
|
} else {
|
||||||
|
el.textContent = 'down';
|
||||||
|
el.dataset.ms = 99999;
|
||||||
|
el.classList.add('glass-slow');
|
||||||
|
}
|
||||||
|
if (--pending === 0) sortGrid();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function sortGrid() {
|
||||||
|
const arr = Array.from(grid.querySelectorAll('.glass-pop'));
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
const aMs = parseInt(a.querySelector('.glass-latency')?.dataset.ms || 99998);
|
||||||
|
const bMs = parseInt(b.querySelector('.glass-latency')?.dataset.ms || 99998);
|
||||||
|
return aMs - bMs;
|
||||||
|
});
|
||||||
|
arr.forEach(el => grid.appendChild(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pingAll() {
|
||||||
|
document.querySelectorAll('.glass-latency[data-status="live"][data-dns]').forEach(el => {
|
||||||
|
const dns = el.dataset.dns;
|
||||||
|
if (!dns) return;
|
||||||
|
el.textContent = '...';
|
||||||
|
el.className = 'glass-val glass-latency';
|
||||||
|
const t0 = performance.now();
|
||||||
|
fetch('https://' + dns + ':1984/ping').then(() => {
|
||||||
|
const ms = Math.round(performance.now() - t0);
|
||||||
|
el.textContent = ms + ' ms';
|
||||||
|
el.dataset.ms = ms;
|
||||||
|
el.classList.add(ms < 100 ? 'glass-fast' : ms < 250 ? 'glass-ok' : 'glass-slow');
|
||||||
|
}).catch(() => {
|
||||||
|
const ms = Math.round(performance.now() - t0);
|
||||||
|
if (ms < 4900) {
|
||||||
|
el.textContent = ms + ' ms';
|
||||||
|
el.dataset.ms = ms;
|
||||||
|
el.classList.add(ms < 100 ? 'glass-fast' : ms < 250 ? 'glass-ok' : 'glass-slow');
|
||||||
|
} else {
|
||||||
|
el.textContent = 'down';
|
||||||
|
el.dataset.ms = 99999;
|
||||||
|
el.classList.add('glass-slow');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setTimeout(sortGrid, 6000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(pingAll, 60000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
|
@ -2,107 +2,19 @@
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<div class="hero container">
|
<div class="hero container">
|
||||||
<p class="label accent mb-4"><span class="vaultname">clavitor</span> hosted</p>
|
<p class="label accent mb-4"><span class="vaultname">clavitor</span> hosted</p>
|
||||||
<h1>Zero cache. Every request hits the vault. So the vault has to be close.</h1>
|
<h1>Zero cache. Every request hits the vault.</h1>
|
||||||
<p class="lead">Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run across 22 regions on every continent. Your data lives where you choose. <s>$20</s> $12/yr.</p>
|
<p class="lead">Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run {{len .Pops}} regions across every continent. Your data lives where you choose. <s>$20</s> $12/yr.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Map -->
|
<!-- Map -->
|
||||||
<div class="section container">
|
<div class="container">
|
||||||
<div class="map-wrap">
|
<div class="map-wrap">
|
||||||
<svg id="worldmap" viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<image href="/worldmap.svg" x="0" y="0" width="1000" height="460"/>
|
||||||
<circle cx="284.7" cy="143.8" r="4" fill="none" stroke="#DC2626" 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>
|
<text x="500" y="440" font-family="Figtree,sans-serif" font-size="18" font-weight="700" fill="#0A0A0A" text-anchor="middle" opacity="0.35" letter-spacing="0.3em">CLAVITOR GLOBAL PRESENCE</text>
|
||||||
<circle cx="284.7" cy="143.8" r="4" fill="none" stroke="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="284.7" y="155.8" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Washington D.C.</text>
|
|
||||||
<circle cx="160.0" cy="143.1" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="160.0" y="135.1" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">San Francisco</text>
|
|
||||||
<circle cx="295.6" cy="122.8" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="295.6" y="114.8" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Montréal</text>
|
|
||||||
<circle cx="224.7" cy="187.0" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="224.7" y="199.0" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Mexico City</text>
|
|
||||||
<circle cx="294.2" cy="219.7" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="294.2" y="231.7" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Bogotá</text>
|
|
||||||
<circle cx="370.6" cy="282.7" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="370.6" y="294.7" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">São Paulo</text>
|
|
||||||
<circle cx="303.9" cy="306.0" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="303.9" y="318.0" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Santiago</text>
|
|
||||||
<circle cx="499.7" cy="106.0" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="499.7" y="98.0" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">London</text>
|
|
||||||
<circle cx="523.6" cy="117.6" r="5" fill="none" stroke="#0A0A0A" 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="#0A0A0A" 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="#0A0A0A" stroke="#F5F5F5" stroke-width="2"/>
|
|
||||||
<circle cx="523.6" cy="117.6" r="3" fill="#F5F5F5"/>
|
|
||||||
<text x="523.6" y="109.6" font-family="Inter,sans-serif" font-size="8.5" fill="#0A0A0A" text-anchor="middle" opacity="0.85">Zürich</text>
|
|
||||||
<circle cx="489.7" cy="136.4" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="489.7" y="128.4" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Madrid</text>
|
|
||||||
<circle cx="550.3" cy="82.1" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="550.3" y="74.1" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Stockholm</text>
|
|
||||||
<circle cx="580.3" cy="134.8" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="580.3" y="126.8" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Istanbul</text>
|
|
||||||
<circle cx="653.6" cy="173.6" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="653.6" y="165.6" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Dubai</text>
|
|
||||||
<circle cx="509.4" cy="215.7" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="509.4" y="227.7" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Lagos</text>
|
|
||||||
<circle cx="602.2" cy="232.8" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="602.2" y="244.8" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Nairobi</text>
|
|
||||||
<circle cx="551.1" cy="307.2" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="551.1" y="319.2" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Cape Town</text>
|
|
||||||
<circle cx="702.5" cy="187.7" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="702.5" y="179.7" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Mumbai</text>
|
|
||||||
<circle cx="788.3" cy="227.2" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="788.3" y="239.2" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Singapore</text>
|
|
||||||
<circle cx="920.0" cy="307.2" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="920.0" y="319.2" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Sydney</text>
|
|
||||||
<circle cx="888.1" cy="148.3" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="888.1" y="140.3" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Tokyo</text>
|
|
||||||
<circle cx="852.5" cy="143.6" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="852.5" y="135.6" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Seoul</text>
|
|
||||||
<circle cx="817.2" cy="180.3" r="4" fill="none" stroke="#DC2626" 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="#DC2626" 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="#DC2626" stroke="#F5F5F5" stroke-width="1.5"/>
|
|
||||||
<text x="817.2" y="172.3" font-family="Inter,sans-serif" font-size="8.5" fill="#B91C1C" text-anchor="middle" opacity="0.85">Hong Kong</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-gap"></div>
|
<div class="mt-12"></div>
|
||||||
<div id="dc-grid" class="mb-8">
|
<div id="dc-grid" class="mb-8">
|
||||||
<!-- Self-hosted -->
|
<!-- Self-hosted -->
|
||||||
<div class="dc-card red" data-lon="-999">
|
<div class="dc-card red" data-lon="-999">
|
||||||
|
|
@ -136,8 +48,8 @@
|
||||||
<!-- Why Zürich -->
|
<!-- Why Zürich -->
|
||||||
<div class="section container">
|
<div class="section container">
|
||||||
<p class="label gold mb-3">Why Zürich</p>
|
<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>
|
<h2 class="mb-4">Identity Encryption: jurisdiction irrelevant.<br>Credential Encryption: 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>
|
<p class="lead mb-8">Identity fields are protected by math — where the server sits doesn't matter. But Credential 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="grid-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p class="label mb-2">Self-hosted · US</p>
|
<p class="label mb-2">Self-hosted · US</p>
|
||||||
|
|
@ -205,6 +117,8 @@
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
const W = 1000, H = 460;
|
const W = 1000, H = 460;
|
||||||
|
const ns = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
function project(lon, lat) {
|
function project(lon, lat) {
|
||||||
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
|
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 miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
|
||||||
|
|
@ -214,17 +128,82 @@
|
||||||
return [Math.round(x*10)/10, Math.round(y*10)/10];
|
return [Math.round(x*10)/10, Math.round(y*10)/10];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPopDot(svg, pop, delay) {
|
||||||
|
const [x, y] = project(pop.lon, pop.lat);
|
||||||
|
const isHQ = pop.city === 'Zürich';
|
||||||
|
const isLive = pop.status === 'live';
|
||||||
|
const dotColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
|
||||||
|
const textColor = isHQ ? '#0A0A0A' : isLive ? '#B91C1C' : '#777777';
|
||||||
|
const pulseColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
|
||||||
|
const dotSize = isHQ ? 11 : 9;
|
||||||
|
const pulseR = isHQ ? 5 : 4;
|
||||||
|
const pulseMax = isHQ ? 18 : 13;
|
||||||
|
const tooltip = isLive ? pop.city + ' · Live' : pop.city + ' · Planned · Q2 2026';
|
||||||
|
|
||||||
|
// Pulse ring 1
|
||||||
|
const r1 = document.createElementNS(ns, 'circle');
|
||||||
|
r1.setAttribute('cx', x); r1.setAttribute('cy', y);
|
||||||
|
r1.setAttribute('r', pulseR); r1.setAttribute('fill', 'none');
|
||||||
|
r1.setAttribute('stroke', pulseColor); r1.setAttribute('stroke-width', isHQ ? '2' : '1.5');
|
||||||
|
const a1 = document.createElementNS(ns, 'animate');
|
||||||
|
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
|
||||||
|
a1.setAttribute('dur', '2.4s'); a1.setAttribute('begin', delay+'s'); a1.setAttribute('repeatCount', 'indefinite');
|
||||||
|
const a2 = document.createElementNS(ns, 'animate');
|
||||||
|
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.6;0;0.6');
|
||||||
|
a2.setAttribute('dur', '2.4s'); a2.setAttribute('begin', delay+'s'); a2.setAttribute('repeatCount', 'indefinite');
|
||||||
|
r1.appendChild(a1); r1.appendChild(a2);
|
||||||
|
|
||||||
|
// Pulse ring 2
|
||||||
|
const r2 = document.createElementNS(ns, 'circle');
|
||||||
|
r2.setAttribute('cx', x); r2.setAttribute('cy', y);
|
||||||
|
r2.setAttribute('r', pulseR); r2.setAttribute('fill', 'none');
|
||||||
|
r2.setAttribute('stroke', pulseColor); r2.setAttribute('stroke-width', isHQ ? '1.5' : '1');
|
||||||
|
const a3 = document.createElementNS(ns, 'animate');
|
||||||
|
a3.setAttribute('attributeName', 'r'); a3.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
|
||||||
|
a3.setAttribute('dur', '2.4s'); a3.setAttribute('begin', (delay+0.8)+'s'); a3.setAttribute('repeatCount', 'indefinite');
|
||||||
|
const a4 = document.createElementNS(ns, 'animate');
|
||||||
|
a4.setAttribute('attributeName', 'stroke-opacity'); a4.setAttribute('values', '0.4;0;0.4');
|
||||||
|
a4.setAttribute('dur', '2.4s'); a4.setAttribute('begin', (delay+0.8)+'s'); a4.setAttribute('repeatCount', 'indefinite');
|
||||||
|
r2.appendChild(a3); r2.appendChild(a4);
|
||||||
|
|
||||||
|
// Square dot
|
||||||
|
const half = dotSize / 2;
|
||||||
|
const dot = document.createElementNS(ns, 'rect');
|
||||||
|
dot.setAttribute('x', x - half); dot.setAttribute('y', y - half);
|
||||||
|
dot.setAttribute('width', dotSize); dot.setAttribute('height', dotSize);
|
||||||
|
dot.setAttribute('fill', dotColor); dot.setAttribute('stroke', '#F5F5F5'); dot.setAttribute('stroke-width', '1.5');
|
||||||
|
const title = document.createElementNS(ns, 'title');
|
||||||
|
title.textContent = tooltip;
|
||||||
|
dot.appendChild(title);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
const label = document.createElementNS(ns, 'text');
|
||||||
|
label.setAttribute('x', x); label.setAttribute('y', y - half - 4);
|
||||||
|
label.setAttribute('font-family', 'Inter,sans-serif');
|
||||||
|
label.setAttribute('font-size', '8.5');
|
||||||
|
label.setAttribute('fill', textColor);
|
||||||
|
label.setAttribute('text-anchor', 'middle');
|
||||||
|
label.setAttribute('opacity', '0.85');
|
||||||
|
label.textContent = pop.city;
|
||||||
|
const labelTitle = document.createElementNS(ns, 'title');
|
||||||
|
labelTitle.textContent = tooltip;
|
||||||
|
label.appendChild(labelTitle);
|
||||||
|
|
||||||
|
svg.appendChild(r1);
|
||||||
|
svg.appendChild(r2);
|
||||||
|
svg.appendChild(dot);
|
||||||
|
svg.appendChild(label);
|
||||||
|
}
|
||||||
|
|
||||||
function addVisitorDot(lat, lon, city) {
|
function addVisitorDot(lat, lon, city) {
|
||||||
const svg = document.getElementById('worldmap');
|
const svg = document.getElementById('worldmap');
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
const [x, y] = project(lon, lat);
|
const [x, y] = project(lon, lat);
|
||||||
const ns = 'http://www.w3.org/2000/svg';
|
|
||||||
|
|
||||||
// Pulse ring
|
|
||||||
const ring = document.createElementNS(ns, 'circle');
|
const ring = document.createElementNS(ns, 'circle');
|
||||||
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
|
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
|
||||||
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
|
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
|
||||||
ring.setAttribute('stroke', '#EF4444'); ring.setAttribute('stroke-width', '1.5');
|
ring.setAttribute('stroke', '#0A0A0A'); ring.setAttribute('stroke-width', '1.5');
|
||||||
const a1 = document.createElementNS(ns, 'animate');
|
const a1 = document.createElementNS(ns, 'animate');
|
||||||
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
|
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
|
||||||
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
|
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
|
||||||
|
|
@ -233,18 +212,16 @@
|
||||||
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
|
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
|
||||||
ring.appendChild(a1); ring.appendChild(a2);
|
ring.appendChild(a1); ring.appendChild(a2);
|
||||||
|
|
||||||
// Dot
|
|
||||||
const dot = document.createElementNS(ns, 'circle');
|
const dot = document.createElementNS(ns, 'circle');
|
||||||
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
|
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
|
||||||
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#EF4444');
|
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#0A0A0A');
|
||||||
dot.setAttribute('stroke', '#0a1628'); dot.setAttribute('stroke-width', '1.5');
|
dot.setAttribute('stroke', '#ffffff'); dot.setAttribute('stroke-width', '1.5');
|
||||||
|
|
||||||
// Label
|
|
||||||
const label = document.createElementNS(ns, 'text');
|
const label = document.createElementNS(ns, 'text');
|
||||||
label.setAttribute('x', x); label.setAttribute('y', y + 15);
|
label.setAttribute('x', x); label.setAttribute('y', y + 15);
|
||||||
label.setAttribute('font-family', 'Inter,sans-serif');
|
label.setAttribute('font-family', 'Inter,sans-serif');
|
||||||
label.setAttribute('font-size', '10');
|
label.setAttribute('font-size', '10');
|
||||||
label.setAttribute('fill', '#EF4444');
|
label.setAttribute('fill', '#0A0A0A');
|
||||||
label.setAttribute('text-anchor', 'middle');
|
label.setAttribute('text-anchor', 'middle');
|
||||||
label.setAttribute('font-weight', '500');
|
label.setAttribute('font-weight', '500');
|
||||||
label.textContent = city || 'You';
|
label.textContent = city || 'You';
|
||||||
|
|
@ -254,31 +231,16 @@
|
||||||
svg.appendChild(label);
|
svg.appendChild(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
const POPS = [
|
const POPS = [{{range .Pops}}
|
||||||
{name:"Washington D.C.", region:"us-east-1", lat:37.5, lon:-77.5},
|
{city:"{{.City}}", region:"{{.RegionName}}", lat:{{.Lat}}, lon:{{.Lon}}, status:"{{.Status}}", provider:"{{.Provider}}"},{{end}}
|
||||||
{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},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Render all POP dots from DB data
|
||||||
|
const svg = document.getElementById('worldmap');
|
||||||
|
if (svg) {
|
||||||
|
POPS.forEach((pop, i) => addPopDot(svg, pop, (i * 0.08).toFixed(2)));
|
||||||
|
}
|
||||||
|
|
||||||
function findClosestPop(lat, lon) {
|
function findClosestPop(lat, lon) {
|
||||||
return POPS.reduce((best, p) => {
|
return POPS.reduce((best, p) => {
|
||||||
const d = (lat-p.lat)**2 + (lon-p.lon)**2;
|
const d = (lat-p.lat)**2 + (lon-p.lon)**2;
|
||||||
|
|
@ -295,7 +257,7 @@
|
||||||
const nameEl = document.getElementById('closest-name');
|
const nameEl = document.getElementById('closest-name');
|
||||||
const subEl = document.getElementById('closest-sub');
|
const subEl = document.getElementById('closest-sub');
|
||||||
const buyEl = document.getElementById('closest-buy');
|
const buyEl = document.getElementById('closest-buy');
|
||||||
if (nameEl) nameEl.textContent = closest.name;
|
if (nameEl) nameEl.textContent = closest.city;
|
||||||
if (subEl) subEl.textContent = d.city ? `~${d.city}` : 'Your region';
|
if (subEl) subEl.textContent = d.city ? `~${d.city}` : 'Your region';
|
||||||
if (buyEl) buyEl.href = `/signup?region=${closest.region}`;
|
if (buyEl) buyEl.href = `/signup?region=${closest.region}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@
|
||||||
<div>
|
<div>
|
||||||
<p class="label accent mb-6">George Orwell — 1984</p>
|
<p class="label accent mb-6">George Orwell — 1984</p>
|
||||||
<h1 class="mb-6">"If you want to keep a secret, you must also hide it from yourself."</h1>
|
<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>
|
<p class="lead mb-6">We did. Your Identity Encryption 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">
|
<div class="btn-row">
|
||||||
<a href="/hosted" class="btn btn-primary">Get hosted — <s>$20</s> $12/yr</a>
|
<a href="/hosted" class="btn btn-primary">Get hosted — <s>$20</s> $12/yr</a>
|
||||||
<a href="/install" class="btn btn-ghost">Self-host free →</a>
|
<a href="/install" class="btn btn-ghost">Self-host free →</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<!-- Hero SVG: L2/L3 split diagram -->
|
<!-- Hero SVG: Credential/Identity encryption diagram -->
|
||||||
<svg viewBox="0 0 480 380" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 480 380" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Background -->
|
<!-- Background -->
|
||||||
<rect x="0" y="0" width="480" height="380" rx="12" fill="#f5f5f5"/>
|
<rect x="0" y="0" width="480" height="380" rx="12" fill="#f5f5f5"/>
|
||||||
|
|
@ -28,10 +28,10 @@
|
||||||
<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>
|
<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>
|
</defs>
|
||||||
|
|
||||||
<!-- L2 Column (AI-readable) -->
|
<!-- Credential Encryption Column (AI-readable) -->
|
||||||
<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="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"/>
|
<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">L2 — AI can read</text>
|
<text x="130" y="85" font-family="JetBrains Mono, monospace" font-size="12" fill="#22C55E" text-anchor="middle" font-weight="600">Credential — AI can read</text>
|
||||||
|
|
||||||
<!-- L2 items -->
|
<!-- L2 items -->
|
||||||
<g>
|
<g>
|
||||||
|
|
@ -59,10 +59,10 @@
|
||||||
<path d="M188 277 L190.5 279.5 L196 274" stroke="#22C55E" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
<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>
|
</g>
|
||||||
|
|
||||||
<!-- L3 Column (sealed) -->
|
<!-- Identity Encryption Column (you only) -->
|
||||||
<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="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"/>
|
<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">L3 — you only</text>
|
<text x="350" y="85" font-family="JetBrains Mono, monospace" font-size="12" fill="#EF4444" text-anchor="middle" font-weight="600">Identity — you only</text>
|
||||||
|
|
||||||
<!-- L3 items -->
|
<!-- L3 items -->
|
||||||
<g>
|
<g>
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
<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>
|
<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="grid-2">
|
||||||
<div class="card alt">
|
<div class="card alt">
|
||||||
<span class="badge accent mb-4">Agent fields</span>
|
<span class="badge accent mb-4">Credential Encryption</span>
|
||||||
<h3 class="mb-3">AI-readable</h3>
|
<h3 class="mb-3">AI-readable</h3>
|
||||||
<p class="mb-4">Encrypted at rest, decryptable by the vault server. Your AI agent reads these via MCP.</p>
|
<p class="mb-4">Encrypted at rest, decryptable by the vault server. Your AI agent reads these via MCP.</p>
|
||||||
<ul class="checklist">
|
<ul class="checklist">
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card red">
|
<div class="card red">
|
||||||
<span class="badge red mb-4">Sealed fields</span>
|
<span class="badge red mb-4">Identity Encryption</span>
|
||||||
<h3 class="mb-3">Touch ID only</h3>
|
<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>
|
<p class="mb-4">Encrypted client-side with WebAuthn PRF. The server never sees the plaintext. Ever.</p>
|
||||||
<ul class="checklist red">
|
<ul class="checklist red">
|
||||||
|
|
@ -174,12 +174,12 @@
|
||||||
<div class="card card-hover">
|
<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>
|
<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>
|
<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>
|
<p>Identity Encryption uses WebAuthn PRF — a cryptographic key derived from your biometric hardware. Math, not policy. We literally cannot decrypt it.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card card-hover">
|
<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>
|
<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>
|
<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>
|
<p>Store TOTP secrets as Credential fields. Your AI generates time-based codes on demand via MCP — no more switching to your phone.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card card-hover">
|
<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>
|
<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>
|
||||||
|
|
@ -194,7 +194,7 @@
|
||||||
<div class="card card-hover">
|
<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>
|
<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>
|
<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>
|
<p>Import from any password manager. The built-in LLM automatically classifies which fields should be Credential vs Identity.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -394,7 +394,7 @@
|
||||||
<h2 class="mb-4">Your vault needs to be everywhere you are.</h2>
|
<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="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-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">clavitor</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>
|
<p class="mb-8">We run <span class="vaultname">clavitor</span> across 28 regions on every continent. <s>$20</s> $12/yr. Your Identity Encryption keys never leave your browser — we mathematically cannot read your private fields.</p>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<a href="/hosted" class="btn btn-primary">Get hosted →</a>
|
<a href="/hosted" class="btn btn-primary">Get hosted →</a>
|
||||||
<a href="/install" class="btn btn-ghost">Self-host anyway</a>
|
<a href="/install" class="btn btn-ghost">Self-host anyway</a>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<div class="step-num">2</div>
|
<div class="step-num">2</div>
|
||||||
<div class="step-body">
|
<div class="step-body">
|
||||||
<h2>Set your vault key</h2>
|
<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>
|
<p>The vault key encrypts your Credential fields at rest. If you lose this key, Credential field data cannot be recovered.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div><span class="comment"># Generate a random key</span></div>
|
<div><span class="comment"># Generate a random key</span></div>
|
||||||
<div><span class="prompt">$</span> export VAULT_KEY=$(openssl rand -hex 32)</div>
|
<div><span class="prompt">$</span> export VAULT_KEY=$(openssl rand -hex 32)</div>
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
<div class="step-num">5</div>
|
<div class="step-num">5</div>
|
||||||
<div class="step-body">
|
<div class="step-body">
|
||||||
<h2>Import your passwords</h2>
|
<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>
|
<p>The LLM classifier automatically suggests Credential/Identity assignments for each field. Review and confirm in the web UI.</p>
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div><span class="comment"># Chrome, Firefox, Bitwarden, Proton Pass, 1Password</span></div>
|
<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 chrome passwords.csv</div>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<p class="mb-6">Forever. MIT license. No strings.</p>
|
<p class="mb-6">Forever. MIT license. No strings.</p>
|
||||||
<a href="/install" class="btn btn-ghost btn-block mb-8">Self-host guide →</a>
|
<a href="/install" class="btn btn-ghost btn-block mb-8">Self-host guide →</a>
|
||||||
<p class="label mb-4">What you get</p>
|
<p class="label mb-4">What you get</p>
|
||||||
<ul class="checklist"><li>Agent & 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>
|
<ul class="checklist"><li>Three-tier encryption (Vault, Credential, Identity)</li><li>WebAuthn PRF (Identity biometric encryption)</li><li>MCP server for AI agents</li><li>Scoped MCP tokens (multi-agent)</li><li>TOTP generation via MCP</li><li>Browser extension (Chrome, Firefox)</li><li>Import from Bitwarden / 1Password</li><li>LLM-powered field classification</li><li>Unlimited entries</li><li>Full source code (MIT)</li></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="price-card featured">
|
<div class="price-card featured">
|
||||||
|
|
@ -25,8 +25,8 @@
|
||||||
<div class="price-amount mb-2"><s>$20</s> $12<span class="price-period">/year</span></div>
|
<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>
|
<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>
|
<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>
|
<p class="label accent 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 & patches</li><li>TLS included</li><li>Email support</li></ul>
|
<ul class="checklist"><li>Managed infrastructure</li><li>Daily encrypted backups</li><li>28 regions across every continent</li><li>Automatic updates & patches</li><li>TLS included</li><li>Email support</li></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,19 +40,19 @@
|
||||||
|
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<h3>Why so cheap?</h3>
|
<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>
|
<p>AI agents are everywhere — and so are the security risks. We set a price that's within reach for everyone, whether you're in the US, Nigeria, or the Philippines. $12/yr is launch pricing; regular price is $20/yr. Join now and your price is locked for life.</p>
|
||||||
|
|
||||||
<h3>Is the self-hosted version missing any features?</h3>
|
<h3>Does self-hosted get every feature?</h3>
|
||||||
<p>No. Every feature ships in both versions. Hosted adds managed infrastructure and backups — not functionality.</p>
|
<p>Yes. Every feature ships in both versions. Hosted adds managed infrastructure and backups — not functionality.</p>
|
||||||
|
|
||||||
<h3>Can hosted clavitor read my Sealed fields?</h3>
|
<h3>Are my Identity fields safe on the hosted server?</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>
|
<p>Yes. Identity 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>
|
<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>
|
<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>
|
<h3>Can I get a refund?</h3>
|
||||||
<p>No free trial — but 7-day money-back, no questions asked, instant refund. That's a stronger guarantee.</p>
|
<p>Yes. 7-day money-back, no questions asked, instant refund.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
|
|
||||||
<h2>The short version</h2>
|
<h2>The short version</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Your vault data is encrypted at rest (Agent fields) and in transit (TLS).</li>
|
<li>Your vault is protected by three encryption layers: Vault Encryption (at rest), Credential Encryption (per-field), and Identity Encryption (client-side). All data is encrypted in transit (TLS).</li>
|
||||||
<li>Sealed fields are encrypted client-side with WebAuthn PRF. We cannot decrypt them. Ever.</li>
|
<li>Identity 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>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>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>
|
<li>You can delete your account and all data at any time.</li>
|
||||||
|
|
@ -27,8 +27,8 @@
|
||||||
<p>When you use hosted clavitor, we store:</p>
|
<p>When you use hosted clavitor, we store:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Account information:</strong> email address and authentication credentials</li>
|
<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>Credential fields:</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>Identity fields:</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>
|
<li><strong>Metadata:</strong> entry creation and modification timestamps, entry titles</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
@ -40,8 +40,8 @@
|
||||||
<li>Cookies beyond session authentication</li>
|
<li>Cookies beyond session authentication</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Sealed field encryption guarantee</h2>
|
<h2>Identity 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>
|
<p>Fields protected by Identity Encryption 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 Identity 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>
|
<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>
|
<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>
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
<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>
|
<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>
|
<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>
|
<p>If compelled by valid legal process, we can only provide: your email address, account creation date, and encrypted vault data. Credential fields are encrypted with your vault key (which we do not store). Identity fields are 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>
|
<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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p class="mb-4">"The L2/L3 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="mb-4">"The Credential/Identity 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 · Hacker News · 2024</p>
|
<p class="label">@homelab_nerd · Hacker News · 2024</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
<div class="sg-section">
|
<div class="sg-section">
|
||||||
<p class="sg-title">Typography</p>
|
<p class="sg-title">Typography</p>
|
||||||
<h1 class="mb-4">h1 — Your vault. Wherever you want it.</h1>
|
<h1 class="mb-4">h1 — Your vault. Wherever you want it.</h1>
|
||||||
<h2 class="mb-4">h2 — Sealed fields: jurisdiction irrelevant.</h2>
|
<h2 class="mb-4">h2 — Identity Encryption: jurisdiction irrelevant.</h2>
|
||||||
<h3 class="mb-4">h3 — Only you. Only in person.</h3>
|
<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="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="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>
|
||||||
|
|
@ -69,8 +69,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card alt">
|
<div class="card alt">
|
||||||
<p class="label accent mb-3">card.alt</p>
|
<p class="label accent mb-3">card.alt</p>
|
||||||
<h3 class="mb-3">Sealed fields</h3>
|
<h3 class="mb-3">Credential fields</h3>
|
||||||
<p>Green-tinted surface. Use for sealed layer content.</p>
|
<p>Green-tinted surface. Use for credential layer content.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card gold">
|
<div class="card gold">
|
||||||
<p class="label gold mb-3">card.gold</p>
|
<p class="label gold mb-3">card.gold</p>
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
<p>By using clavitor (the "Service"), you agree to these terms. If you don't agree, don't use the Service.</p>
|
<p>By using clavitor (the "Service"), you agree to these terms. If you don't agree, don't use the Service.</p>
|
||||||
|
|
||||||
<h2>2. Description</h2>
|
<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>
|
<p>clavitor is a password manager with three-tier encryption: Vault Encryption (at rest), Credential Encryption (per-field, server-side), and Identity Encryption (per-field, client-side). The hosted service stores encrypted vault data on your behalf. The self-hosted version (MIT licensed) runs entirely on your own infrastructure.</p>
|
||||||
|
|
||||||
<h2>3. Accounts</h2>
|
<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>
|
<p>You are responsible for maintaining the security of your account credentials and authenticator device. We cannot recover Identity fields if you lose access to your WebAuthn authenticator — the mathematical design prevents it.</p>
|
||||||
|
|
||||||
<h2>4. Acceptable use</h2>
|
<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>
|
<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>
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<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>
|
<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>
|
<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>
|
<p>Credential fields (server-encrypted) provide strong encryption at rest and in transit. Identity 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>
|
<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>
|
<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>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
|
@ -44,6 +44,13 @@ func NewRouter(cfg *lib.Config, webFS embed.FS, templateFS embed.FS) *chi.Mux {
|
||||||
// Health check (unauthenticated — no Bearer needed)
|
// Health check (unauthenticated — no Bearer needed)
|
||||||
r.Get("/health", h.Health)
|
r.Get("/health", h.Health)
|
||||||
|
|
||||||
|
// Ping — minimal latency probe for looking glass (no DB, no auth)
|
||||||
|
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Content-Length", "11")
|
||||||
|
w.Write([]byte(`{"ok":true}`))
|
||||||
|
})
|
||||||
|
|
||||||
// Auth endpoints (unauthenticated — no Bearer, DB found by glob)
|
// Auth endpoints (unauthenticated — no Bearer, DB found by glob)
|
||||||
r.Get("/api/auth/status", h.AuthStatus)
|
r.Get("/api/auth/status", h.AuthStatus)
|
||||||
r.Post("/api/auth/register/begin", h.AuthRegisterBegin)
|
r.Post("/api/auth/register/begin", h.AuthRegisterBegin)
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue