35c6ff5a71
Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"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 {
|
|
writeProblem(w, r, http.StatusMethodNotAllowed, problemMethodNotAllowed,
|
|
"POST only", nil)
|
|
return
|
|
}
|
|
if !requireCore(w, r, core, "presence ingest") {
|
|
return
|
|
}
|
|
key := r.URL.Query().Get("key")
|
|
source, ok := presenceSignals[key]
|
|
if !ok {
|
|
writeProblem(w, r, http.StatusBadRequest, problemInvalidRequest,
|
|
"unknown signal key", nil)
|
|
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 {
|
|
writeProblem(w, r, http.StatusBadGateway, problemCoreWriteFailed,
|
|
"write failed", fmt.Errorf("write presence signal %q: %w", key, err))
|
|
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 {
|
|
writeProblem(w, r, http.StatusMethodNotAllowed, problemMethodNotAllowed,
|
|
"POST only", nil)
|
|
return
|
|
}
|
|
if !requireCore(w, r, core, "revert") {
|
|
return
|
|
}
|
|
if !stepUpGate(w, r, session, requireStepUp) {
|
|
return
|
|
}
|
|
|
|
key := strings.TrimSpace(r.FormValue("key"))
|
|
if key == "" {
|
|
writeProblem(w, r, http.StatusBadRequest, problemInvalidRequest,
|
|
"key required", nil)
|
|
return
|
|
}
|
|
newID, err := core.RevertFact(r.Context(), key)
|
|
if err != nil {
|
|
if errors.Is(err, ipc.ErrNoFact) {
|
|
writeProblem(w, r, http.StatusNotFound, problemResourceNotFound,
|
|
"no fact to revert", fmt.Errorf("revert fact %q: %w", key, err))
|
|
return
|
|
}
|
|
writeProblem(w, r, http.StatusBadGateway, problemCoreChangeFailed,
|
|
"revert failed", fmt.Errorf("revert fact %q: %w", key, err))
|
|
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})
|
|
}
|