nrc_events.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "context"
7 "crypto/rand"
8 "encoding/json"
9 "fmt"
10 "time"
11
12 "next.orly.dev/pkg/lol/chk"
13 "next.orly.dev/pkg/lol/log"
14
15 "next.orly.dev/pkg/nostr/crypto/keys"
16 "next.orly.dev/pkg/nostr/encoders/event"
17 "next.orly.dev/pkg/nostr/encoders/filter"
18 "next.orly.dev/pkg/nostr/encoders/hex"
19 "next.orly.dev/pkg/nostr/encoders/kind"
20 "next.orly.dev/pkg/nostr/encoders/tag"
21 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
22 )
23
24 // NRC connection event kind - using application-specific data range
25 // Kind 30078 is commonly used for app-specific data
26 const KindNRCConnection = uint16(30078)
27
28 // NRCEventStore provides NRC connection management using events.
29 // This works with any Database implementation that supports SaveEvent/QueryEvents.
30 type NRCEventStore struct {
31 db Database
32 relaySigner *p8k.Signer
33 relayPubkey []byte
34 }
35
36 // NewNRCEventStore creates a new event-based NRC store.
37 // relaySigner is used to sign NRC connection events.
38 func NewNRCEventStore(db Database, relaySigner *p8k.Signer) *NRCEventStore {
39 return &NRCEventStore{
40 db: db,
41 relaySigner: relaySigner,
42 relayPubkey: relaySigner.Pub(),
43 }
44 }
45
46 // nrcConnectionContent is the JSON structure stored in event content
47 type nrcConnectionContent struct {
48 ID string `json:"id"`
49 Label string `json:"label"`
50 Secret string `json:"secret"` // hex encoded
51 DerivedPubkey string `json:"derived_pubkey"` // hex encoded
52 RendezvousURL string `json:"rendezvous_url"` // WebSocket URL of rendezvous relay
53 CreatedAt int64 `json:"created_at"`
54 LastUsed int64 `json:"last_used"`
55 CreatedBy string `json:"created_by"` // hex encoded
56 }
57
58 // CreateNRCConnection generates a new NRC connection with a random secret.
59 func (s *NRCEventStore) CreateNRCConnection(label string, rendezvousURL string, createdBy []byte) (*NRCConnection, error) {
60 // Generate random 32-byte secret
61 secret := make([]byte, 32)
62 if _, err := rand.Read(secret); err != nil {
63 return nil, fmt.Errorf("failed to generate random secret: %w", err)
64 }
65
66 // Derive pubkey from secret
67 derivedPubkey, err := keys.SecretBytesToPubKeyBytes(secret)
68 if err != nil {
69 return nil, fmt.Errorf("failed to derive pubkey from secret: %w", err)
70 }
71
72 // Use first 8 bytes of secret as ID (hex encoded = 16 chars)
73 id := string(hex.Enc(secret[:8]))
74
75 conn := &NRCConnection{
76 ID: id,
77 Label: label,
78 Secret: secret,
79 DerivedPubkey: derivedPubkey,
80 RendezvousURL: rendezvousURL,
81 CreatedAt: time.Now().Unix(),
82 LastUsed: 0,
83 CreatedBy: createdBy,
84 }
85
86 if err := s.SaveNRCConnection(conn); chk.E(err) {
87 return nil, err
88 }
89
90 log.I.F("created NRC connection: id=%s label=%s rendezvous=%s", id, label, rendezvousURL)
91 return conn, nil
92 }
93
94 // SaveNRCConnection stores an NRC connection as an event.
95 func (s *NRCEventStore) SaveNRCConnection(conn *NRCConnection) error {
96 // Create content JSON
97 content := nrcConnectionContent{
98 ID: conn.ID,
99 Label: conn.Label,
100 Secret: string(hex.Enc(conn.Secret)),
101 DerivedPubkey: string(hex.Enc(conn.DerivedPubkey)),
102 RendezvousURL: conn.RendezvousURL,
103 CreatedAt: conn.CreatedAt,
104 LastUsed: conn.LastUsed,
105 }
106 if len(conn.CreatedBy) > 0 {
107 content.CreatedBy = string(hex.Enc(conn.CreatedBy))
108 }
109
110 contentJSON, err := json.Marshal(content)
111 if err != nil {
112 return fmt.Errorf("failed to marshal NRC connection: %w", err)
113 }
114
115 // Create replaceable event with d-tag = connection ID
116 // Use "p" tag for derived_pubkey since only single-letter tags are indexed
117 ev := &event.E{
118 Kind: KindNRCConnection,
119 CreatedAt: time.Now().Unix(),
120 Tags: tag.NewS(
121 tag.NewFromAny("d", conn.ID),
122 tag.NewFromAny("p", string(hex.Enc(conn.DerivedPubkey))), // derived pubkey for authorization lookup
123 ),
124 Content: contentJSON,
125 }
126
127 // Sign with relay identity
128 if err := ev.Sign(s.relaySigner); err != nil {
129 return fmt.Errorf("failed to sign NRC connection event: %w", err)
130 }
131
132 // Save to database
133 ctx := context.Background()
134 if _, err := s.db.SaveEvent(ctx, ev); err != nil {
135 return fmt.Errorf("failed to save NRC connection event: %w", err)
136 }
137
138 return nil
139 }
140
141 // GetNRCConnection retrieves an NRC connection by ID.
142 func (s *NRCEventStore) GetNRCConnection(id string) (*NRCConnection, error) {
143 ctx := context.Background()
144
145 // Query for the specific connection event
146 limit := uint(1)
147 f := &filter.F{
148 Kinds: kind.NewS(kind.New(KindNRCConnection)),
149 Authors: tag.NewFromBytesSlice(s.relayPubkey),
150 Tags: tag.NewS(tag.NewFromAny("d", id)),
151 Limit: &limit,
152 }
153
154 events, err := s.db.QueryEvents(ctx, f)
155 if err != nil {
156 return nil, err
157 }
158 if len(events) == 0 {
159 return nil, fmt.Errorf("NRC connection not found: %s", id)
160 }
161
162 return s.eventToConnection(events[0])
163 }
164
165 // GetNRCConnectionByDerivedPubkey retrieves an NRC connection by its derived pubkey.
166 func (s *NRCEventStore) GetNRCConnectionByDerivedPubkey(derivedPubkey []byte) (*NRCConnection, error) {
167 ctx := context.Background()
168 pubkeyHex := string(hex.Enc(derivedPubkey))
169
170 // Query by "p" tag (derived pubkey) - single-letter tags are indexed
171 limit := uint(1)
172 f := &filter.F{
173 Kinds: kind.NewS(kind.New(KindNRCConnection)),
174 Authors: tag.NewFromBytesSlice(s.relayPubkey),
175 Tags: tag.NewS(tag.NewFromAny("p", pubkeyHex)),
176 Limit: &limit,
177 }
178
179 events, err := s.db.QueryEvents(ctx, f)
180 if err != nil {
181 return nil, err
182 }
183 if len(events) == 0 {
184 return nil, fmt.Errorf("NRC connection not found for pubkey")
185 }
186
187 return s.eventToConnection(events[0])
188 }
189
190 // DeleteNRCConnection removes an NRC connection by deleting its event.
191 func (s *NRCEventStore) DeleteNRCConnection(id string) error {
192 ctx := context.Background()
193
194 // Query to find the event
195 limit := uint(1)
196 f := &filter.F{
197 Kinds: kind.NewS(kind.New(KindNRCConnection)),
198 Authors: tag.NewFromBytesSlice(s.relayPubkey),
199 Tags: tag.NewS(tag.NewFromAny("d", id)),
200 Limit: &limit,
201 }
202
203 events, err := s.db.QueryEvents(ctx, f)
204 if err != nil {
205 return err
206 }
207 if len(events) == 0 {
208 return nil // Already deleted
209 }
210
211 // Delete the event
212 return s.db.DeleteEvent(ctx, events[0].ID)
213 }
214
215 // GetAllNRCConnections returns all NRC connections.
216 func (s *NRCEventStore) GetAllNRCConnections() ([]*NRCConnection, error) {
217 ctx := context.Background()
218
219 // Query all NRC connection events from this relay
220 f := &filter.F{
221 Kinds: kind.NewS(kind.New(KindNRCConnection)),
222 Authors: tag.NewFromBytesSlice(s.relayPubkey),
223 }
224
225 events, err := s.db.QueryEvents(ctx, f)
226 if err != nil {
227 return nil, err
228 }
229
230 conns := make([]*NRCConnection, 0, len(events))
231 for _, ev := range events {
232 conn, err := s.eventToConnection(ev)
233 if err != nil {
234 log.W.F("failed to parse NRC connection event: %v", err)
235 continue
236 }
237 conns = append(conns, conn)
238 }
239
240 return conns, nil
241 }
242
243 // GetNRCAuthorizedSecrets returns a map of derived pubkeys to labels.
244 func (s *NRCEventStore) GetNRCAuthorizedSecrets() (map[string]string, error) {
245 conns, err := s.GetAllNRCConnections()
246 if err != nil {
247 return nil, err
248 }
249
250 result := make(map[string]string)
251 for _, conn := range conns {
252 pubkeyHex := string(hex.Enc(conn.DerivedPubkey))
253 result[pubkeyHex] = conn.Label
254 }
255
256 return result, nil
257 }
258
259 // UpdateNRCConnectionLastUsed updates the last used timestamp.
260 func (s *NRCEventStore) UpdateNRCConnectionLastUsed(id string) error {
261 conn, err := s.GetNRCConnection(id)
262 if err != nil {
263 return err
264 }
265
266 conn.LastUsed = time.Now().Unix()
267 return s.SaveNRCConnection(conn)
268 }
269
270 // GetNRCConnectionURI generates the full connection URI for a connection.
271 // Uses the rendezvous URL stored in the connection.
272 func (s *NRCEventStore) GetNRCConnectionURI(conn *NRCConnection, relayPubkey []byte) (string, error) {
273 if len(relayPubkey) != 32 {
274 return "", fmt.Errorf("invalid relay pubkey length: %d", len(relayPubkey))
275 }
276 if conn.RendezvousURL == "" {
277 return "", fmt.Errorf("connection has no rendezvous URL")
278 }
279
280 relayPubkeyHex := hex.Enc(relayPubkey)
281 secretHex := hex.Enc(conn.Secret)
282
283 uri := fmt.Sprintf("nostr+relayconnect://%s?relay=%s&secret=%s",
284 relayPubkeyHex, conn.RendezvousURL, secretHex)
285
286 if conn.Label != "" {
287 uri += fmt.Sprintf("&name=%s", conn.Label)
288 }
289
290 return uri, nil
291 }
292
293 // eventToConnection parses an event into an NRCConnection.
294 func (s *NRCEventStore) eventToConnection(ev *event.E) (*NRCConnection, error) {
295 var content nrcConnectionContent
296 if err := json.Unmarshal(ev.Content, &content); err != nil {
297 return nil, fmt.Errorf("failed to unmarshal NRC connection content: %w", err)
298 }
299
300 secret, err := hex.Dec(content.Secret)
301 if err != nil {
302 return nil, fmt.Errorf("failed to decode secret: %w", err)
303 }
304
305 derivedPubkey, err := hex.Dec(content.DerivedPubkey)
306 if err != nil {
307 return nil, fmt.Errorf("failed to decode derived pubkey: %w", err)
308 }
309
310 var createdBy []byte
311 if content.CreatedBy != "" {
312 createdBy, _ = hex.Dec(content.CreatedBy)
313 }
314
315 return &NRCConnection{
316 ID: content.ID,
317 Label: content.Label,
318 Secret: secret,
319 DerivedPubkey: derivedPubkey,
320 RendezvousURL: content.RendezvousURL,
321 CreatedAt: content.CreatedAt,
322 LastUsed: content.LastUsed,
323 CreatedBy: createdBy,
324 }, nil
325 }
326
327 // NRCEventAuthorizer wraps NRCEventStore to implement the NRC authorization interface.
328 type NRCEventAuthorizer struct {
329 store *NRCEventStore
330 }
331
332 // NewNRCEventAuthorizer creates a new NRC authorizer from an event store.
333 func NewNRCEventAuthorizer(store *NRCEventStore) *NRCEventAuthorizer {
334 return &NRCEventAuthorizer{store: store}
335 }
336
337 // GetNRCClientByPubkey looks up an authorized client by their derived pubkey.
338 func (a *NRCEventAuthorizer) GetNRCClientByPubkey(derivedPubkey []byte) (id string, label string, found bool, err error) {
339 conn, err := a.store.GetNRCConnectionByDerivedPubkey(derivedPubkey)
340 if err != nil {
341 // Not found is not an error, just means not authorized
342 return "", "", false, nil
343 }
344 if conn == nil {
345 return "", "", false, nil
346 }
347 return conn.ID, conn.Label, true, nil
348 }
349
350 // UpdateNRCClientLastUsed updates the last used timestamp for tracking.
351 func (a *NRCEventAuthorizer) UpdateNRCClientLastUsed(id string) error {
352 return a.store.UpdateNRCConnectionLastUsed(id)
353 }
354