feat: initial scaffold of uniweb — unibus web frontend (SPA + gateway)
Extracted from unibus v0.13.0: the chat SPA (web/, React+Mantine, per-user BIP39 wallet) and the web gateway (cmd/webgw, REST+SSE) that acts as a bus peer for the browser. Consumes unibus as a Go module via replace => ../unibus, keeping its own replace fn-registry for the cybersecurity primitives. go build/vet/test and pnpm build green in the new location.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cs "fn-registry/functions/cybersecurity"
|
||||
)
|
||||
|
||||
// session is one logged-in browser. In the wallet model each session carries the
|
||||
// user's OWN bus identity: the browser unlocks its locally-encrypted private key
|
||||
// and hands the full keypair to the gateway over TLS, and the gateway spins up a
|
||||
// dedicated bus client (a *gateway) that acts AS that user. The private key lives
|
||||
// only in this process's memory for the life of the session — it is never written
|
||||
// to disk and is dropped when the session ends.
|
||||
//
|
||||
// A session may instead point at the shared operator gateway (the legacy
|
||||
// passphrase login); `owned` distinguishes the two so logout only closes the bus
|
||||
// client it created.
|
||||
type session struct {
|
||||
gw *gateway
|
||||
owned bool // true => gw was built for this session and must be Closed on logout
|
||||
handle string
|
||||
issuedAt time.Time
|
||||
}
|
||||
|
||||
// sessionStore is the gateway's set of live browser sessions, keyed by the opaque
|
||||
// cookie token. It is independent of any single bus identity.
|
||||
type sessionStore struct {
|
||||
mu sync.Mutex
|
||||
m map[string]*session
|
||||
}
|
||||
|
||||
func newSessionStore() *sessionStore { return &sessionStore{m: map[string]*session{}} }
|
||||
|
||||
func (st *sessionStore) put(token string, s *session) {
|
||||
st.mu.Lock()
|
||||
st.m[token] = s
|
||||
st.mu.Unlock()
|
||||
}
|
||||
|
||||
func (st *sessionStore) get(token string) (*session, bool) {
|
||||
st.mu.Lock()
|
||||
defer st.mu.Unlock()
|
||||
s, ok := st.m[token]
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// drop removes a session and returns it so the caller can close an owned gateway.
|
||||
func (st *sessionStore) drop(token string) (*session, bool) {
|
||||
st.mu.Lock()
|
||||
defer st.mu.Unlock()
|
||||
s, ok := st.m[token]
|
||||
if ok {
|
||||
delete(st.m, token)
|
||||
}
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// closeAll closes every owned per-user gateway (used at shutdown). The shared
|
||||
// operator gateway is owned by main and closed separately.
|
||||
func (st *sessionStore) closeAll() {
|
||||
st.mu.Lock()
|
||||
defer st.mu.Unlock()
|
||||
for tok, s := range st.m {
|
||||
if s.owned && s.gw != nil {
|
||||
_ = s.gw.Close()
|
||||
}
|
||||
delete(st.m, tok)
|
||||
}
|
||||
}
|
||||
|
||||
// identityFromHex builds a cs.Identity from the four hex halves the browser sends
|
||||
// on POST /api/session. It enforces the exact key sizes (sign_pub 32, sign_priv
|
||||
// 64, kex_pub 32, kex_priv 32) so a malformed body cannot produce a half-built
|
||||
// identity that fails opaquely deep in the bus client.
|
||||
func identityFromHex(signPub, signPriv, kexPub, kexPriv string) (cs.Identity, error) {
|
||||
sp, err := hex.DecodeString(signPub)
|
||||
if err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("sign_pub: %w", err)
|
||||
}
|
||||
spriv, err := hex.DecodeString(signPriv)
|
||||
if err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("sign_priv: %w", err)
|
||||
}
|
||||
kp, err := hex.DecodeString(kexPub)
|
||||
if err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("kex_pub: %w", err)
|
||||
}
|
||||
kpriv, err := hex.DecodeString(kexPriv)
|
||||
if err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("kex_priv: %w", err)
|
||||
}
|
||||
if len(sp) != 32 || len(spriv) != 64 || len(kp) != 32 || len(kpriv) != 32 {
|
||||
return cs.Identity{}, fmt.Errorf("wrong key sizes (sign_pub=%d sign_priv=%d kex_pub=%d kex_priv=%d; want 32/64/32/32)",
|
||||
len(sp), len(spriv), len(kp), len(kpriv))
|
||||
}
|
||||
return cs.Identity{SignPub: sp, SignPriv: spriv, KexPub: kp, KexPriv: kpriv}, nil
|
||||
}
|
||||
|
||||
// sessionReq is the POST /api/session body: the user's full wallet identity (hex)
|
||||
// plus a display handle. The private halves arrive only over TLS and are held in
|
||||
// memory for the session; they are never persisted server-side.
|
||||
type sessionReq struct {
|
||||
Handle string `json:"handle"`
|
||||
SignPub string `json:"sign_pub"`
|
||||
SignPriv string `json:"sign_priv"`
|
||||
KexPub string `json:"kex_pub"`
|
||||
KexPriv string `json:"kex_priv"`
|
||||
}
|
||||
|
||||
// handleSession opens a per-user session. It builds the user's bus identity from
|
||||
// the posted keypair, connects a dedicated bus client as that user, and issues a
|
||||
// session cookie bound to it. This is the wallet-model replacement for the
|
||||
// operator passphrase login.
|
||||
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
|
||||
var req sessionReq
|
||||
if !decode(w, r, &req) {
|
||||
return
|
||||
}
|
||||
id, err := identityFromHex(req.SignPub, req.SignPriv, req.KexPub, req.KexPriv)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "bad identity: "+err.Error())
|
||||
return
|
||||
}
|
||||
cfg := s.busTemplate
|
||||
cfg.Identity = id
|
||||
gw, err := newGateway(cfg)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadGateway, "connect bus as user: "+err.Error())
|
||||
return
|
||||
}
|
||||
tok := newToken()
|
||||
s.sessions.put(tok, &session{gw: gw, owned: true, handle: req.Handle, issuedAt: time.Now()})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookie,
|
||||
Value: tok,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, meResp{Endpoint: gw.endpoint, SignPub: req.SignPub, Handle: req.Handle})
|
||||
}
|
||||
Reference in New Issue
Block a user