package api // Signed claims (meta-core v2.0.0, docs/implementation/metacore-claims-model.md). // // POST /api/claims the only metadata write path // GET /api/meta/{cid}/claims every live claim of the cid's group // GET /api/metadata/{rid}/claims the same, addressed by rid // POST /api/merge stateless merge of claims the caller holds // // meta-core never signs a claim here: it verifies each one (MMUR2, low-S) and // the signature is the write authentication. import ( "encoding/json" "net/http" "github.com/gorilla/mux" "github.com/metazla/meta-core/internal/claim" "github.com/metazla/meta-core/internal/storage" ) // GoneResponse is the body of every removed unsigned write route. type GoneResponse struct { Error string `json:"error"` Use string `json:"use"` } // handleGone answers the unsigned write routes removed in v2.0.0. func (s *Server) handleGone(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusGone, GoneResponse{Error: "gone", Use: "POST /api/claims"}) } // decodeClaims accepts a JSON array of claims or {"claims": [...]}. func decodeClaims(r *http.Request) ([]claim.Claim, error) { var raw json.RawMessage if err := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 64<<20)).Decode(&raw); err != nil { return nil, err } var claims []claim.Claim if len(raw) > 0 && raw[0] == '{' { var env struct { Claims []claim.Claim `json:"claims"` } if err := json.Unmarshal(raw, &env); err != nil { return nil, err } return env.Claims, nil } if err := json.Unmarshal(raw, &claims); err != nil { return nil, err } return claims, nil } // handlePostClaims handles POST /api/claims: one status per claim, in input // order. 200 even when some claims are refused — the statuses say which. func (s *Server) handlePostClaims(w http.ResponseWriter, r *http.Request) { if !s.storage.IsConnected() { writeError(w, http.StatusServiceUnavailable, "storage not connected") return } claims, err := decodeClaims(r) if err != nil { writeErrorSlug(w, http.StatusBadRequest, "invalid_json", "body must be a JSON array of claims: "+err.Error(), false) return } if len(claims) > storage.MaxClaimsPerBatch { writeErrorSlug(w, http.StatusRequestEntityTooLarge, "too_many_claims", "at most 10000 claims per batch", false) return } results, err := s.storage.WriteClaims(r.Context(), claims) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, results) } // ClaimsResponse is the body of the claims read routes: the record's rid, the // cids of its group, and every live signed claim on them. type ClaimsResponse struct { RID string `json:"rid"` CIDs []string `json:"cids"` Claims []claim.Claim `json:"claims"` } // handleClaimsByCID handles GET /api/meta/{cid}/claims[?key=…]. func (s *Server) handleClaimsByCID(w http.ResponseWriter, r *http.Request) { cid := mux.Vars(r)["cid"] if !s.storage.IsConnected() { writeError(w, http.StatusServiceUnavailable, "storage not connected") return } rid, err := s.storage.GetByCID(cid) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if rid == "" { writeErrorSlug(w, http.StatusNotFound, ErrUnknownCID, "no record for this CID", false) return } s.writeClaims(w, r, rid) } // handleClaimsByRID handles GET /api/metadata/{rid}/claims[?key=…]. The rid // may also be a cid (resolved like every /api/metadata read). func (s *Server) handleClaimsByRID(w http.ResponseWriter, r *http.Request) { if !s.storage.IsConnected() { writeError(w, http.StatusServiceUnavailable, "storage not connected") return } rid := s.storage.ResolveRoot(mux.Vars(r)["hashId"]) s.writeClaims(w, r, rid) } // writeClaims answers both claims routes: ?key= keeps one exact key, ?uid= // one signer; 404 when the record does not exist. func (s *Server) writeClaims(w http.ResponseWriter, r *http.Request, rid string) { cids, claims, err := s.storage.RecordClaims(rid, r.URL.Query().Get("key")) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if len(cids) == 0 { writeErrorSlug(w, http.StatusNotFound, ErrUnknownCID, "no record for this rid or cid", false) return } out := make([]claim.Claim, 0, len(claims)) uid := r.URL.Query().Get("uid") for _, cl := range claims { if uid == "" || cl.UID == uid { out = append(out, cl) } } writeJSON(w, http.StatusOK, ClaimsResponse{RID: rid, CIDs: cids, Claims: out}) } // MergeRequest is the body of POST /api/merge. type MergeRequest struct { Claims []claim.Claim `json:"claims"` As string `json:"as,omitempty"` } // MergeResponse is the merged field map, the number of agreeing signers per // key, and how many claims failed verification (dropped, never an error). type MergeResponse struct { Fields map[string]string `json:"fields"` Nsrc map[string]int `json:"nsrc"` Rejected int `json:"rejected"` } // handleMerge handles POST /api/merge: verify, merge per key with the same // rule the cache uses, overlay `as`'s own votes held by this core. No write. func (s *Server) handleMerge(w http.ResponseWriter, r *http.Request) { var req MergeRequest if err := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 64<<20)).Decode(&req); err != nil { writeErrorSlug(w, http.StatusBadRequest, "invalid_json", err.Error(), false) return } if len(req.Claims) > storage.MaxClaimsPerBatch { writeErrorSlug(w, http.StatusRequestEntityTooLarge, "too_many_claims", "at most 10000 claims per merge", false) return } fields, nsrc, rejected, err := s.storage.MergeClaims(req.Claims, req.As) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, MergeResponse{Fields: fields, Nsrc: nsrc, Rejected: rejected}) }