package main import ( "encoding/json" "errors" "log" "net/http" "strings" "time" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/webauthn" ) // The two fact-writing API routes: POST /api/signal appends a presence // observation, POST /api/revert voids the latest fact for a key. Neither // renders a page. // presenceSignals — the only fact keys /api/signal may write. mavweb is a // network-facing surface inside wg; an allowlist keeps a compromised caller // boxed to forging weak presence signals (reachability, multi-source, never // truth) — it can't write arbitrary facts. ponytail: floor auth (wg-only); a // per-signal token belongs here if the tunnel ever hosts untrusted devices. var presenceSignals = map[string]string{ "desk_active": "infer:hyprland", "page_heartbeat": "infer:heartbeat", "wg_handshake": "infer:wg", } // handleSignal ingests one presence signal and writes a fresh fact through // CoreAPI. The fact's timestamp (now) is all the presence scorer reads; value // is a marker. Only allowlisted keys are accepted (see presenceSignals). func handleSignal(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } if !requireCore(w, core, "presence ingest") { return } key := r.URL.Query().Get("key") source, ok := presenceSignals[key] if !ok { http.Error(w, "unknown signal key", http.StatusBadRequest) return } // kind=env: an observation about the device/surface, NOT a self-fact — a // passive signal never writes truth about you (spec), it only feeds // presence. confidence 1.0: the reading ("input happened") is certain; // presence applies its own per-signal weight/decay on top. if _, err := core.WriteFact(r.Context(), ipc.WriteFactReq{ Ts: time.Now(), Kind: "env", Key: key, Value: `"active"`, Source: source, Confidence: 1.0, }); err != nil { log.Printf("signal %s: %v", key, err) http.Error(w, "write failed", http.StatusBadGateway) return } w.WriteHeader(http.StatusNoContent) } func handleRevert(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, session *webauthn.PasskeySession, requireStepUp bool) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } if !requireCore(w, core, "revert") { return } if !stepUpGate(w, session, requireStepUp) { return } key := strings.TrimSpace(r.FormValue("key")) if key == "" { http.Error(w, "key required", http.StatusBadRequest) return } newID, err := core.RevertFact(r.Context(), key) if err != nil { log.Printf("revert %q: %v", key, err) if errors.Is(err, ipc.ErrNoFact) { http.Error(w, "no fact to revert", http.StatusNotFound) return } http.Error(w, "revert failed", http.StatusBadGateway) return } log.Printf("reverted fact for key=%s, new_id=%d", key, newID) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"reverted": true, "new_id": newID}) }