clone

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

main
tinyrelay / tinygit / gitrelay.go
gitrelay.go 6.2K
1package tinygit23import (4 "context"5 "errors"6 "fmt"7 "net/http"8 "os"9 "strings"10 "sync"1112 event "github.com/FelineStateMachine/tinyrelay/protocol/nostr"13)1415// Repository is the resolved authority and Git state for one hosted project.16type Repository struct {17 Owner string18 Identifier string19 EventID string20 Private bool21 Clone []string22 Relays []string23 Maintainers []string24 Refs map[string]string25 Head string26 Alternative bool27}2829// Maintainer is one key that may sign repository state, push refs and change30// status. Role is "owner", "maintainer" for a key in the announcement's31// maintainers tag, or "agent" for a key the host vouches for under an agent32// grant; Name carries the grant's label when it has one.33type Maintainer struct {34 PubKey string `json:"pubkey"`35 Role string `json:"role"`36 Name string `json:"name,omitempty"`37}3839// MaintainerSource answers whether a key maintains a repository beyond the40// owner and the announcement's maintainers tag. The host relay supplies it so41// authority it records elsewhere, such as an agent grant, counts in every42// place this package checks maintainers.43type MaintainerSource interface {44 IsMaintainer(ctx context.Context, r Repository, pubkey string) bool45}4647// Config supplies the event store, Git root, authorization and transport hooks.48type Config struct {49 Store Store50 Root string51 Policy func() Policy52 Authorize func(context.Context, Event, Repository) error53 // Maintainers extends maintainer checks with host-side authority. It is54 // consulted only after the owner and the maintainers tag.55 Maintainers MaintainerSource56 // AuthorizeHTTP is called for private repositories and for operators that57 // want signed/NIP-98 authorization at the Git HTTP boundary.58 AuthorizeHTTP func(context.Context, *http.Request, Repository) error59 // ServiceURL is used to reject recursive GRASP sources and to construct60 // advertised clone URLs. It is optional for an embedded relay.61 ServiceURL string62 // PublicURL is the canonical HTTPS URL used in repository advertisements;63 // ServiceURL is used when PublicURL is empty.64 PublicURL string65 // EnableGRASP06 enables the alternative PR repository surface. It is kept66 // opt-in because publishing a capability creates an interoperability67 // promise beyond the base GRASP-01 HTTP endpoint.68 EnableGRASP06 bool69 // AllowMissingObjects is ignored.70 // Missing objects enter the durable pending path and stay hidden until71 // receive-pack or repair supplies every advertised tip.72 AllowMissingObjects bool73 // AllowPrivateRelays permits operator-controlled private DNS targets for74 // self-hosted networks. Public deployments should leave it disabled.75 AllowPrivateRelays bool76 // PrivatePeers are operator-configured GRASP-08 origins. Only these77 // sources may receive signed outbound Git requests.78 PrivatePeers []string79 // HTTPAuth signs requests to configured private peers. It is never used for80 // sources that are merely present in a repository announcement.81 HTTPAuth HTTPAuthSigner82 // GitSync and EventSync are operator-owned transport callbacks. Keeping83 // transport outside this package allows native websocket/NIP-77, HTTP, or84 // an internal queue without making GRASP depend on one client library.85 GitSync func(context.Context, Repository) error86 EventSync func(context.Context, Repository) error87 // OnPromote is called after a pending state becomes object-complete and88 // visible. The host relay uses it to release event visibility/live fanout.89 OnPromote func(context.Context, string, Repository) error90}9192// GitRelay coordinates signed repository events with native bare repositories.93type GitRelay struct {94 store Store95 root string96 policy func() Policy97 authorize func(context.Context, event.Event, Repository) error98 authorizeHTTP func(context.Context, *http.Request, Repository) error99 maintainers MaintainerSource100 serviceURL string101 gitSync func(context.Context, Repository) error102 eventSync func(context.Context, Repository) error103 onPromote func(context.Context, string, Repository) error104 grasp06 bool105 allowMissing bool106 allowPrivate bool107 privatePeers []string108 httpAuth HTTPAuthSigner109 mu sync.RWMutex110 repos map[string]Repository111 pending map[string]struct{}112 commitMu sync.Mutex113 tickMu sync.Mutex114 repoConfigMu sync.Mutex115 configured map[string]struct{}116}117118func New(cfg Config) (*GitRelay, error) {119 if cfg.Store == nil {120 return nil, errors.New("git relay: store is required")121 }122 store := cfg.Store123 if cfg.Root == "" {124 return nil, errors.New("git relay: root is required")125 }126 if err := os.MkdirAll(cfg.Root, 0700); err != nil {127 return nil, fmt.Errorf("git relay root: %w", err)128 }129 p := cfg.Policy130 if p == nil {131 p = func() Policy { return DefaultPolicy("") }132 }133 serviceURL := cfg.PublicURL134 if serviceURL == "" {135 serviceURL = cfg.ServiceURL136 }137 g := &GitRelay{store: store, root: cfg.Root, policy: p, authorize: cfg.Authorize, authorizeHTTP: cfg.AuthorizeHTTP, maintainers: cfg.Maintainers, serviceURL: strings.TrimRight(serviceURL, "/"), grasp06: cfg.EnableGRASP06, allowMissing: cfg.AllowMissingObjects, allowPrivate: cfg.AllowPrivateRelays, privatePeers: append([]string(nil), cfg.PrivatePeers...), httpAuth: cfg.HTTPAuth, gitSync: cfg.GitSync, eventSync: cfg.EventSync, onPromote: cfg.OnPromote, repos: make(map[string]Repository), pending: make(map[string]struct{})}138 if err := g.recoverJournals(); err != nil {139 return nil, err140 }141 if err := g.Reload(context.Background()); err != nil {142 return nil, err143 }144 return g, nil145}146147func key(owner, identifier string) string { return owner + "\x00" + identifier }148149// IsMaintainer reports whether pubkey may sign state and push refs for a150// repository: the owner, a key in the announcement's maintainers tag, or a key151// the configured MaintainerSource vouches for. Every maintainer check in this152// package goes through here so the host's answer applies uniformly.153func (g *GitRelay) IsMaintainer(ctx context.Context, r Repository, pubkey string) bool {154 if pubkey == "" {155 return false156 }157 if pubkey == r.Owner || contains(r.Maintainers, pubkey) {158 return true159 }160 return g.maintainers != nil && g.maintainers.IsMaintainer(ctx, r, pubkey)161}