mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-26 18:37:43 +02:00
harden(mana-sync): fix WebSocket auth, add validation, tests, and docs
Critical security and correctness fixes for the sync server: Security: - Fix WebSocket JWT validation — was completely broken (hardcoded "pending-auth"). Now validates JWT via JWKS, rejects invalid tokens, enforces 10-second auth deadline, sends auth-ok confirmation. - Add 10 MB request body size limit (prevents OOM attacks) - Validate op field (must be insert/update/delete) - Validate table and id fields (must be non-empty) - Abort sync on RecordChange failure (was silently continuing) Correctness: - Fix silent JSON unmarshal errors in store (now returns error) - Copy client set before iterating in NotifyUser (prevents race) - Add write timeout on WebSocket notifications Testing (19 tests, 0 -> 100% for unit-testable code): - auth: token extraction, validator init, missing auth handling - config: defaults, env override, invalid port - sync: op validation, changeset validation, response format, field change round-trip, body size constant Documentation: - Add CLAUDE.md with architecture, sync protocol, LWW explanation, API endpoints, configuration, security notes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0848ea1b3
commit
4ff3ceb01a
8 changed files with 760 additions and 32 deletions
|
|
@ -2,6 +2,7 @@ package sync
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
|
@ -23,6 +24,12 @@ func NewHandler(s *store.Store, v *auth.Validator, h *ws.Hub) *Handler {
|
|||
return &Handler{store: s, validator: v, hub: h}
|
||||
}
|
||||
|
||||
// maxBodySize is the maximum allowed request body (10 MB).
|
||||
const maxBodySize = 10 * 1024 * 1024
|
||||
|
||||
// validOps are the allowed sync operation types.
|
||||
var validOps = map[string]bool{"insert": true, "update": true, "delete": true}
|
||||
|
||||
// HandleSync processes a POST /sync/:appId request.
|
||||
// Receives a changeset from a client, records changes, and returns the server delta.
|
||||
func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -45,6 +52,9 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Limit request body size
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
|
||||
|
||||
// Parse changeset
|
||||
var changeset Changeset
|
||||
if err := json.NewDecoder(r.Body).Decode(&changeset); err != nil {
|
||||
|
|
@ -52,6 +62,18 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Validate changes
|
||||
for i, change := range changeset.Changes {
|
||||
if !validOps[change.Op] {
|
||||
http.Error(w, fmt.Sprintf("invalid op %q in change %d", change.Op, i), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if change.Table == "" || change.ID == "" {
|
||||
http.Error(w, fmt.Sprintf("missing table or id in change %d", i), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
clientID := r.Header.Get("X-Client-Id")
|
||||
if clientID == "" {
|
||||
|
|
@ -87,7 +109,8 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
|
|||
err := h.store.RecordChange(ctx, appID, change.Table, change.ID, userID, change.Op, clientID, data, fieldTimestamps)
|
||||
if err != nil {
|
||||
slog.Error("failed to record change", "error", err, "table", change.Table, "id", change.ID)
|
||||
// Continue processing other changes
|
||||
http.Error(w, "failed to record change: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue