Files
meta-core/internal/api/claims.go
T

173 lines
5.8 KiB
Go

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})
}