clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / tinygit / metadata.go
metadata.go 3.6K
1package tinygit23import (4 "errors"5 "fmt"6 "strings"78 "github.com/FelineStateMachine/tinyrelay/protocol/nostr"9)1011// Metadata contains the claims parsed from a repository announcement (kind12// 30617) or state (kind 30618). Author is the event's claimed signer, not a13// resolved repository owner. Announcement fields are not inherited by states.14// A Metadata value conveys neither signature validity nor hosting authority.15type Metadata struct {16 Kind int17 Author string18 Identifier string19 EventID string20 Private bool21 Clone []string22 Relays []string23 Maintainers []string24 Refs map[string]string25 Head string26}2728// ParseMetadata checks and parses tinygit's supported repository metadata29// shape: identifier, state ref names and nonzero SHA-1 tips, and symbolic HEAD30// syntax. HEAD may name a branch or tag not present in Refs, including an unborn31// branch. Unknown tags and supported extension fields are ignored.32//33// This is a pure shape/semantic check, not full NIP or GRASP compliance. It does34// not validate the event ID or signature, consult announcements or maintainers,35// authorize hosting, check clone or relay endpoints, or inspect Git objects.36// Use nostr.Validate separately before trusting Author. GitRelay.Validate and37// ValidateImported combine signature validation, this parser and host admission;38// missing Git objects remain a separate pending-state concern.39func ParseMetadata(e nostr.Event) (Metadata, error) {40 if e.Kind != 30617 && e.Kind != 30618 {41 return Metadata{}, fmt.Errorf("unsupported: event kind %d is not GRASP repository metadata", e.Kind)42 }43 id := nostr.Tag(e, "d")44 if !validIdentifier(id) {45 return Metadata{}, errors.New("invalid: repository identifier")46 }47 m := Metadata{Kind: e.Kind, Author: e.PubKey, Identifier: id, EventID: e.ID, Refs: map[string]string{}}48 if e.Kind == 30617 {49 for _, tag := range e.Tags {50 if len(tag) < 2 {51 continue52 }53 switch tag[0] {54 case "private":55 m.Private = tag[1] == "true"56 case "clone":57 m.Clone = append(m.Clone, tag[1:]...)58 case "relays":59 m.Relays = append(m.Relays, tag[1:]...)60 case "maintainers":61 m.Maintainers = append(m.Maintainers, tag[1:]...)62 }63 }64 return m, nil65 }66 for _, tag := range e.Tags {67 if len(tag) == 0 {68 continue69 }70 if tag[0] == "HEAD" {71 if len(tag) != 2 || m.Head != "" {72 return Metadata{}, errors.New("invalid: repository state has multiple HEAD tags")73 }74 m.Head = tag[1]75 continue76 }77 if len(tag) < 2 {78 continue79 }80 if strings.HasPrefix(tag[0], "refs/") {81 if _, duplicate := m.Refs[tag[0]]; duplicate || tag[1] == "" || !isObjectID(tag[1]) || strings.Trim(tag[1], "0") == "" {82 return Metadata{}, fmt.Errorf("invalid: ref %s is not a SHA-1", tag[0])83 }84 m.Refs[tag[0]] = tag[1]85 }86 }87 if err := validateRefs(m.Refs); err != nil {88 return Metadata{}, err89 }90 if err := validateHead(m.Head, m.Refs); err != nil {91 return Metadata{}, err92 }93 return m, nil94}9596func validateHead(head string, refs map[string]string) error {97 if head == "" {98 return nil99 }100 if !strings.HasPrefix(head, "ref: ") {101 return errors.New("invalid: HEAD must use ref: syntax")102 }103 ref := strings.TrimPrefix(head, "ref: ")104 if !strings.HasPrefix(ref, "refs/heads/") && !strings.HasPrefix(ref, "refs/tags/") {105 return errors.New("invalid: HEAD must name a branch or tag")106 }107 if !validRef(ref) {108 return errors.New("invalid: HEAD ref")109 }110 return nil111}112113func validateRefs(refs map[string]string) error {114 for ref, oid := range refs {115 if !validRef(ref) || oid != "" && !isObjectID(oid) {116 return fmt.Errorf("invalid: ref %s", ref)117 }118 }119 return nil120}