diff --git a/hosted.html b/hosted.html index f65ade0..67e7f0b 100644 --- a/hosted.html +++ b/hosted.html @@ -50,126 +50,191 @@ -
-
-

vault1984 Hosted

-

We run it. You own it. Your L2 keys never leave your device.

+ +
+
+
+

Hosting

+

Your vault. Wherever you want it.

+

We run it. You own it. Pick your region — your data stays there.

+
- -
+ +
-

Your vault. Deployed close to you.

-

Hosted on Hostkey TIER III infrastructure. Pick your region at signup.

-

Or run it yourself. We embrace that too. No account, no payment, no questions asked.

- -
-

The security model

-

Location is latency. Not security.

-

- Your L2 encryption key is derived client-side from your Touch ID or security key. It never leaves your device. Our servers store ciphertext they cannot decrypt — regardless of who owns the rack, the building, or the country it sits in. -

-

- Pick your region for speed. Pick it for compliance if your organisation requires it. But your private fields are safe in any of them. -

-
- -
+
+ Washington D.C. + San Francisco + Montréal - - + Mexico City + + + + Bogotá + + - - + São Paulo + + + + Santiago + + - - - - - - + London + Zürich + Madrid - - + Stockholm + + + + Istanbul + + - - - - - + Dubai + + + + Lagos + + + + Nairobi + + - - + Cape Town + + - - + Mumbai + + - - - - - - + Singapore - - + Sydney + + + + Tokyo + + - - + Seoul + + + Hong Kong + +
- -
-
-
🇺🇸
-
Virginia
-
US East
-
Live
+ +
+
🖥️
+
Self-hosted
+
Your machine. Your rules.
+
Free forever
+ Download now →
-
-
🇨🇭
-
Zürich
-
EU Central
-
Live
+ +
+
🇨🇭
+
Zürich, Switzerland
+
Capital of Security
+
Headquarters
+ Buy now →
-
-
🇦🇺
-
Sydney
-
Asia Pacific South
-
Live
+ +
+
📍
+
Nearest region
+
Locating you…
+
Closest to you
+ Buy now → +
+
+
+
+ + +
+
+ +
+

The security model

+

Your AI needs access.
Not to everything.

+

A password manager that blocks AI agents is useless in 2025. But one that hands them everything is a liability. vault1984 solves this with two layers.

+
+ +
+
+

Sealed fields

+

Only you. Only in person.

+

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. Not a court order. Not your AI assistant. Sealed fields require your physical presence to unlock.

+
+
+

Agent fields

+

Your AI, scoped and controlled.

+

Fields you designate as agent-accessible are encrypted on our servers. You issue scoped tokens — Claude gets your GitHub token, nothing else. Revoke at any time. The agent never sees sealed fields, no matter what.

+
+
+ +
+

Why Zürich

+

Sealed fields: jurisdiction irrelevant.
Agent fields: it isn't.

+

Sealed fields are protected by math — the server's location doesn't matter. But agent fields live on a server in a jurisdiction. A US server is subject to the CLOUD Act. A UK server to the Investigatory Powers Act. Zürich is subject to Swiss law — which does not cooperate with foreign government data requests. No backdoors. Both layers protected.

+
+
+

Self-hosted · US

+

Your server, your rules — until a court says otherwise. CLOUD Act applies to US persons regardless of encryption.

+
+
+

Self-hosted · anywhere

+

Full control. Your infrastructure, your jurisdiction. The right choice if you know what you're doing.

+
+
+

Hosted · Zürich, Switzerland

+

Swiss law. Swiss courts. No CLOUD Act. No backdoors. We handle the infrastructure — you get the protection.

+
- +

Your agent and you — same vault, right access

@@ -424,67 +489,75 @@ 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 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 city = d.city || 'You'; - const country = d.country_name || ''; - const region = d.region || ''; - - const card = document.createElement('div'); - card.className = 'rounded-xl p-4 text-center card-hover'; - card.setAttribute('data-lon', d.longitude); - card.style.cssText = 'background:#1f0a0a;border:1px solid rgba(239,68,68,0.35);flex:1;min-width:0'; - card.innerHTML = ` -
${flag}
-
${city}
-
${country}
-
${region}
-
- You are here -
`; - - // 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); - - + 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}`; } - 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(() => {}); + // 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(); + } })(); diff --git a/main.go b/main.go index 4e1c90c..feb0437 100644 --- a/main.go +++ b/main.go @@ -2,22 +2,44 @@ package main import ( "embed" + "io" "log" "net/http" "os" + "strings" ) //go:embed *.html *.svg *.css var static embed.FS +func geoHandler(w http.ResponseWriter, r *http.Request) { + ip := r.Header.Get("X-Forwarded-For") + if ip == "" { + ip = r.RemoteAddr + } + // Strip port + 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() { port := os.Getenv("PORT") if port == "" { port = "8099" } - + http.HandleFunc("/geo", geoHandler) http.Handle("/", http.FileServer(http.FS(static))) - log.Printf("vault1984-web starting on :%s", port) if err := http.ListenAndServe(":"+port, nil); err != nil { log.Fatal(err) diff --git a/vault1984-web b/vault1984-web index d10e00f..1d1ea04 100755 Binary files a/vault1984-web and b/vault1984-web differ