tinyrelayfile
tinyrelay/internal/daemon/capabilities.go/

internal/daemon/capabilities.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/event"
12 "github.com/FelineStateMachine/tinyrelay/internal/policy"
13 "github.com/FelineStateMachine/tinyrelay/internal/replication"
14 "github.com/FelineStateMachine/tinyrelay/internal/storage"
15)
16
17// Capability describes an implemented protocol surface and its operational
18// status. Status is intentionally explicit so NIP-11 or an operator UI does
19// not advertise a feature merely because a name appears in a registry.
20type Capability struct {
21 ID string `json:"id"`
22 Status string `json:"status"`
23 Reason string `json:"reason,omitempty"`
24 Profiles []string `json:"profiles,omitempty"`
25}
26
27func capabilityNIPNumber(id string) (int, bool) {
28 if !strings.HasPrefix(id, "NIP-") {
29 return 0, false
30 }
31 number, err := strconv.Atoi(strings.TrimPrefix(id, "NIP-"))
32 return number, err == nil
33}
34
35// Capabilities returns the capability registry for a tenant. Optional
36// operator peer liveness is passed explicitly and is never inferred by
37// crawling public relays.
38func (t *Tenant) Capabilities(peers *PeerMonitor) []Capability {
39 if t == nil {
40 return nil
41 }
42 p := t.Policy()
43 capabilities := CapabilityRegistryWithServices(p, t.git != nil, t.blobs != nil && p.Features.Files, peers)
44 if t.community != nil {
45 setCapability(capabilities, "NIP-43", Capability{ID: "NIP-43", Status: "enabled", Reason: "membership protocol"})
46 setCapability(capabilities, "NIP-56", Capability{ID: "NIP-56", Status: "enabled", Reason: "moderation reports"})
47 if active, err := t.community.ActiveAgentCount(context.Background(), time.Now().Unix()); err == nil && active > 0 {
48 capabilities = append(capabilities, Capability{ID: "agents", Status: "enabled", Reason: "agent identities under owner-signed grants"})
49 }
50 }
51 if t.blobs != nil && p.Features.Files {
52 setCapability(capabilities, "NIP-94", Capability{ID: "NIP-94", Status: "enabled", Reason: "file metadata descriptors"})
53 setCapability(capabilities, "NIP-96", Capability{ID: "NIP-96", Status: "enabled", Reason: "HTTP file storage"})
54 }
55 for index := range capabilities {
56 if capabilities[index].ID == "GRASP" && t.git != nil {
57 capabilities[index].Profiles = t.git.SupportedGRASPs()
58 }
59 }
60 return capabilities
61}
62
63func CapabilityRegistry(p policy.Policy, gitAvailable bool, peers *PeerMonitor) []Capability {
64 return CapabilityRegistryWithServices(p, gitAvailable, false, peers)
65}
66
67func CapabilityRegistryWithServices(p policy.Policy, gitAvailable, blobsAvailable bool, peers *PeerMonitor) []Capability {
68 capabilities := []Capability{
69 {ID: "NIP-01", Status: "enabled", Reason: "relay protocol"},
70 {ID: "NIP-09", Status: "enabled", Reason: "event deletion"},
71 {ID: "NIP-11", Status: "enabled", Reason: "relay information document"},
72 {ID: "NIP-13", Status: "enabled", Reason: "proof of work policy"},
73 {ID: "NIP-17", Status: "enabled", Reason: "private direct messages"},
74 {ID: "NIP-40", Status: "enabled", Reason: "expiration tags"},
75 {ID: "NIP-42", Status: "enabled", Reason: "authenticated relay sessions"},
76 {ID: "NIP-62", Status: "enabled", Reason: "request to vanish"},
77 {ID: "NIP-67", Status: "enabled", Reason: "EOSE completeness hints"},
78 {ID: "NIP-70", Status: "enabled", Reason: "protected event policy"},
79 {ID: "NIP-86", Status: "enabled", Reason: "operator management methods"},
80 {ID: "NIP-98", Status: "enabled", Reason: "HTTP authentication"},
81 {ID: "NIP-43", Status: "disabled", Reason: "membership service is not mounted"},
82 {ID: "NIP-56", Status: "disabled", Reason: "moderation report service is not mounted"},
83 {ID: "NIP-94", Status: "disabled", Reason: "file service is not mounted"},
84 {ID: "NIP-96", Status: "disabled", Reason: "file service is not mounted"},
85 {ID: "NIP-34", Status: "disabled", Reason: "Git repository support is not enabled"},
86 {ID: "GRASP", Status: "disabled", Reason: "GRASP is not enabled"},
87 {ID: "BUD-01", Status: "enabled", Reason: "blob retrieval and basic server behavior"},
88 {ID: "BUD-02", Status: "enabled", Reason: "blob upload and management"},
89 {ID: "BUD-04", Status: "enabled", Reason: "blob mirroring"},
90 {ID: "BUD-06", Status: "enabled", Reason: "upload preflight and storage allowances"},
91 {ID: "BUD-08", Status: "enabled", Reason: "Nostr file metadata in blob descriptors"},
92 {ID: "BUD-09", Status: "enabled", Reason: "blob reports"},
93 {ID: "BUD-11", Status: "enabled", Reason: "Nostr authorization"},
94 {ID: "BUD-12", Status: "enabled", Reason: "cursor-paginated blob listing"},
95 {ID: "BUD-13", Status: "enabled", Reason: "draft hash-addressed uploads and remote sources"},
96 {ID: "BUD-14", Status: "enabled", Reason: "draft multipart uploads with bounded partial storage"},
97 {ID: "NIP-66", Status: "disabled", Reason: "operator peer liveness is not configured"},
98 }
99 if p.Features.Count {
100 capabilities = append(capabilities, Capability{ID: "NIP-45", Status: "enabled", Reason: "count queries"})
101 }
102 if p.Features.Search != "off" {
103 capabilities = append(capabilities, Capability{ID: "NIP-50", Status: "enabled", Reason: "search mode " + p.Features.Search})
104 }
105 if p.Features.Sync {
106 capabilities = append(capabilities, Capability{ID: "NIP-77", Status: "enabled", Reason: "negentropy synchronization"})
107 }
108 if p.Features.Names {
109 capabilities = append(capabilities, Capability{ID: "NIP-05", Status: "enabled", Reason: "name resolution"})
110 }
111 if p.Features.Signer {
112 capabilities = append(capabilities, Capability{ID: "NIP-46", Status: "enabled", Reason: "remote signer"})
113 }
114 if gitAvailable && p.Features.Grasp {
115 setCapability(capabilities, "NIP-34", Capability{ID: "NIP-34", Status: "enabled", Reason: "Git repository support"})
116 profiles := []string{"GRASP-01"}
117 if p.Features.Grasp02 {
118 profiles = append(profiles, "GRASP-02")
119 if p.Features.Grasp03 {
120 profiles = append(profiles, "GRASP-03")
121 }
122 }
123 if p.Features.Grasp06 {
124 profiles = append(profiles, "GRASP-06")
125 }
126 if privatePolicy(p) {
127 profiles = append(profiles, "GRASP-08")
128 }
129 setCapability(capabilities, "GRASP", Capability{ID: "GRASP", Status: "enabled", Reason: "configured GRASP profiles", Profiles: profiles})
130 } else if gitAvailable {
131 setCapability(capabilities, "NIP-34", Capability{ID: "NIP-34", Status: "configured", Reason: "Git support is available; GRASP policy is disabled"})
132 }
133 if peers != nil {
134 status := "configured"
135 reason := "configured peers are monitored; signed publication is not configured"
136 if peers.PublicationEnabled() {
137 status, reason = "enabled", "configured peer liveness publication is enabled"
138 }
139 setCapability(capabilities, "NIP-66", Capability{ID: "NIP-66", Status: status, Reason: reason})
140 }
141 if !blobsAvailable {
142 for index := range capabilities {
143 if strings.HasPrefix(capabilities[index].ID, "BUD-") {
144 capabilities[index].Status = "disabled"
145 capabilities[index].Reason = "file service is not mounted"
146 }
147 }
148 } else {
149 setCapability(capabilities, "NIP-94", Capability{ID: "NIP-94", Status: "enabled", Reason: "file metadata descriptors"})
150 setCapability(capabilities, "NIP-96", Capability{ID: "NIP-96", Status: "enabled", Reason: "HTTP file storage"})
151 }
152 return capabilities
153}
154
155// PublishPeerStatus creates the relay's signed NIP-66 discovery record and
156// stores it locally. Delivery is delegated to the normal durable outbox when
157// the tenant has delivery enabled; no peer is contacted by this method.
158func (t *Tenant) PublishPeerStatus(ctx context.Context, statuses []PeerStatus) error {
159 if t == nil || t.records == nil {
160 return errors.New("daemon: tenant records are not ready")
161 }
162 content, err := json.Marshal(map[string]any{"relay": t.RelayURL(), "peers": statuses})
163 if err != nil {
164 return err
165 }
166 now := time.Now().Unix()
167 e, err := t.records.GenerateEphemeral(ctx, event.KIND_RELAY_DISCOVERY, [][]string{{"d", t.RelayURL()}}, string(content), now)
168 if err != nil {
169 return err
170 }
171 return t.generatedRelayRecord(ctx, e)
172}
173
174func (t *Tenant) generatedRelayRecord(ctx context.Context, e event.Event) error {
175 ctx, done, err := t.beginOperation(ctx)
176 if err != nil {
177 return err
178 }
179 defer done()
180 _, err = t.router.CommitGenerated(ctx, func(ctx context.Context) (event.Event, error) {
181 opts := storage.SaveOptions{Now: e.CreatedAt, SearchMode: t.Policy().Features.Search}
182 if t.replication != nil {
183 opts.Intents = t.replication.Prepare(e, replication.OriginServer)
184 }
185 _, saveErr := t.store.Save(ctx, e, opts)
186 return e, saveErr
187 })
188 return err
189}
190
191func setCapability(capabilities []Capability, id string, value Capability) {
192 for index := range capabilities {
193 if capabilities[index].ID == id {
194 capabilities[index] = value
195 return
196 }
197 }
198}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs