bridge.go raw
1 package nrc
2
3 import (
4 "context"
5 "crypto/rand"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "sync"
10 "time"
11
12 "next.orly.dev/pkg/nostr/crypto/encryption"
13 "next.orly.dev/pkg/nostr/encoders/event"
14 "next.orly.dev/pkg/nostr/encoders/filter"
15 "next.orly.dev/pkg/nostr/encoders/hex"
16 "next.orly.dev/pkg/nostr/encoders/kind"
17 "next.orly.dev/pkg/nostr/encoders/tag"
18 "next.orly.dev/pkg/nostr/encoders/timestamp"
19 "next.orly.dev/pkg/nostr/interfaces/signer"
20 "next.orly.dev/pkg/nostr/ws"
21 "next.orly.dev/pkg/lol/chk"
22 "next.orly.dev/pkg/lol/log"
23 )
24
25 const (
26 // KindNRCRequest is the event kind for NRC requests.
27 KindNRCRequest = 24891
28 // KindNRCResponse is the event kind for NRC responses.
29 KindNRCResponse = 24892
30 // MaxChunkSize is the maximum size for a single chunk (40KB to stay under 65KB limit after NIP-44 + base64).
31 MaxChunkSize = 40000
32 )
33
34 // NRCAuthorizer defines the interface for NRC authorization lookups.
35 // This allows the bridge to look up authorized clients dynamically from the database.
36 type NRCAuthorizer interface {
37 // GetNRCClientByPubkey looks up an authorized client by their derived pubkey.
38 // Returns the client ID, label, and whether the client was found.
39 // If not found, returns empty strings and false.
40 GetNRCClientByPubkey(derivedPubkey []byte) (id string, label string, found bool, err error)
41 // UpdateNRCClientLastUsed updates the last used timestamp for tracking.
42 UpdateNRCClientLastUsed(id string) error
43 }
44
45 // BridgeConfig holds configuration for the NRC bridge.
46 type BridgeConfig struct {
47 // RendezvousURL is the WebSocket URL of the public relay.
48 RendezvousURL string
49 // LocalRelayURL is the WebSocket URL of the local private relay.
50 LocalRelayURL string
51 // Signer is the relay's signer for signing response events.
52 Signer signer.I
53 // AuthorizedSecrets maps derived pubkeys to device names (secret-based auth).
54 // Used when Authorizer is nil.
55 AuthorizedSecrets map[string]string
56 // Authorizer provides dynamic NRC authorization lookups from database.
57 // If set, this takes precedence over AuthorizedSecrets.
58 Authorizer NRCAuthorizer
59 // SessionTimeout is the inactivity timeout for sessions.
60 SessionTimeout time.Duration
61 }
62
63 // Bridge connects a private relay to a public rendezvous relay.
64 type Bridge struct {
65 config *BridgeConfig
66 sessions *SessionManager
67
68 // rendezvousConn is the connection to the rendezvous relay.
69 rendezvousConn *ws.Client
70
71 // mu protects connection state.
72 mu sync.RWMutex
73
74 // ctx is the bridge context.
75 ctx context.Context
76 // cancel cancels the bridge context.
77 cancel context.CancelFunc
78 }
79
80 // NewBridge creates a new NRC bridge.
81 func NewBridge(config *BridgeConfig) *Bridge {
82 ctx, cancel := context.WithCancel(context.Background())
83 timeout := config.SessionTimeout
84 if timeout == 0 {
85 timeout = DefaultSessionTimeout
86 }
87 return &Bridge{
88 config: config,
89 sessions: NewSessionManager(timeout),
90 ctx: ctx,
91 cancel: cancel,
92 }
93 }
94
95 // Start starts the bridge and begins listening for NRC requests.
96 func (b *Bridge) Start() error {
97 log.I.F("starting NRC bridge, rendezvous: %s, local: %s",
98 b.config.RendezvousURL, b.config.LocalRelayURL)
99
100 // Start session cleanup goroutine
101 go b.cleanupLoop()
102
103 // Start the main bridge loop with auto-reconnection
104 go b.runLoop()
105
106 return nil
107 }
108
109 // Stop stops the bridge.
110 func (b *Bridge) Stop() {
111 log.I.F("stopping NRC bridge")
112 b.cancel()
113 b.sessions.Close()
114
115 b.mu.Lock()
116 defer b.mu.Unlock()
117 if b.rendezvousConn != nil {
118 b.rendezvousConn.Close()
119 }
120 }
121
122 // UpdateAuthorizedSecrets updates the map of authorized secrets.
123 // This allows dynamic management of authorized connections through the UI.
124 func (b *Bridge) UpdateAuthorizedSecrets(secrets map[string]string) {
125 b.mu.Lock()
126 defer b.mu.Unlock()
127 b.config.AuthorizedSecrets = secrets
128 }
129
130 // cleanupLoop periodically cleans up expired sessions.
131 func (b *Bridge) cleanupLoop() {
132 ticker := time.NewTicker(5 * time.Minute)
133 defer ticker.Stop()
134
135 for {
136 select {
137 case <-b.ctx.Done():
138 return
139 case <-ticker.C:
140 removed := b.sessions.CleanupExpired()
141 if removed > 0 {
142 log.D.F("cleaned up %d expired NRC sessions", removed)
143 }
144 }
145 }
146 }
147
148 // runLoop runs the main bridge loop with auto-reconnection.
149 func (b *Bridge) runLoop() {
150 delay := time.Second
151
152 for {
153 select {
154 case <-b.ctx.Done():
155 return
156 default:
157 }
158
159 err := b.runOnce()
160 if err != nil {
161 if b.ctx.Err() != nil {
162 return // Context cancelled, exit cleanly
163 }
164 log.W.F("NRC bridge error: %v, reconnecting in %v", err, delay)
165 select {
166 case <-time.After(delay):
167 if delay < 30*time.Second {
168 delay *= 2
169 }
170 case <-b.ctx.Done():
171 return
172 }
173 continue
174 }
175 delay = time.Second
176 }
177 }
178
179 // runOnce runs a single iteration of the bridge.
180 func (b *Bridge) runOnce() error {
181 // Connect to rendezvous relay
182 rendezvousConn, err := ws.RelayConnect(b.ctx, b.config.RendezvousURL)
183 if chk.E(err) {
184 return fmt.Errorf("%w: %v", ErrRendezvousConnectionFailed, err)
185 }
186 defer rendezvousConn.Close()
187
188 b.mu.Lock()
189 b.rendezvousConn = rendezvousConn
190 b.mu.Unlock()
191
192 // Subscribe to NRC request events
193 relayPubkeyHex := hex.Enc(b.config.Signer.Pub())
194 sub, err := rendezvousConn.Subscribe(
195 b.ctx,
196 filter.NewS(&filter.F{
197 Kinds: kind.NewS(kind.New(KindNRCRequest)),
198 Tags: tag.NewS(
199 tag.NewFromAny("p", relayPubkeyHex),
200 ),
201 Since: ×tamp.T{V: time.Now().Unix()},
202 }),
203 )
204 if chk.E(err) {
205 return fmt.Errorf("subscription failed: %w", err)
206 }
207 defer sub.Unsub()
208
209 log.I.F("NRC bridge listening for requests on %s", b.config.RendezvousURL)
210
211 // Process incoming request events
212 for {
213 select {
214 case <-b.ctx.Done():
215 return nil
216 case ev := <-sub.Events:
217 if ev == nil {
218 return fmt.Errorf("subscription closed")
219 }
220 go b.handleRequest(ev)
221 }
222 }
223 }
224
225 // handleRequest handles a single NRC request event.
226 func (b *Bridge) handleRequest(ev *event.E) {
227 ctx, cancel := context.WithTimeout(b.ctx, 30*time.Second)
228 defer cancel()
229
230 // Extract session ID from tags
231 sessionID := ""
232 sessionTag := ev.Tags.GetFirst([]byte("session"))
233 if sessionTag != nil && sessionTag.Len() >= 2 {
234 sessionID = string(sessionTag.Value())
235 }
236 if sessionID == "" {
237 log.W.F("NRC request missing session tag from %s", hex.Enc(ev.Pubkey[:]))
238 return
239 }
240
241 // Verify authorization
242 conversationKey, authMode, deviceName, err := b.authorize(ctx, ev)
243 if err != nil {
244 log.W.F("NRC authorization failed for %s: %v", hex.Enc(ev.Pubkey[:]), err)
245 b.sendError(ctx, ev, sessionID, "unauthorized: "+err.Error())
246 return
247 }
248
249 // Get or create session
250 session := b.sessions.GetOrCreate(sessionID, ev.Pubkey[:], conversationKey, authMode, deviceName)
251 session.Touch()
252
253 // Decrypt request content
254 decrypted, err := encryption.Decrypt(conversationKey, string(ev.Content))
255 if err != nil {
256 log.W.F("NRC decryption failed: %v", err)
257 b.sendError(ctx, ev, sessionID, "decryption failed")
258 return
259 }
260
261 // Parse request message
262 reqMsg, err := ParseRequestContent([]byte(decrypted))
263 if err != nil {
264 log.W.F("NRC invalid request format: %v", err)
265 b.sendError(ctx, ev, sessionID, "invalid request format")
266 return
267 }
268
269 log.D.F("NRC request: type=%s session=%s from=%s",
270 reqMsg.Type, sessionID, hex.Enc(ev.Pubkey[:]))
271
272 // Forward to local relay and handle response
273 if err := b.forwardToLocalRelay(ctx, session, ev, reqMsg); err != nil {
274 log.W.F("NRC forward failed: %v", err)
275 b.sendError(ctx, ev, sessionID, "relay error: "+err.Error())
276 }
277 }
278
279 // authorize checks if the request is authorized and returns the conversation key.
280 func (b *Bridge) authorize(ctx context.Context, ev *event.E) (conversationKey []byte, authMode AuthMode, deviceName string, err error) {
281 clientPubkey := ev.Pubkey[:]
282 clientPubkeyHex := string(hex.Enc(clientPubkey))
283
284 // Try database-backed authorization first (if Authorizer is set)
285 if b.config.Authorizer != nil {
286 clientID, clientLabel, found, authErr := b.config.Authorizer.GetNRCClientByPubkey(clientPubkey)
287 if authErr == nil && found {
288 // Client is authorized via database
289 conversationKey, err = encryption.GenerateConversationKey(
290 b.config.Signer.Sec(),
291 clientPubkey,
292 )
293 if chk.E(err) {
294 return
295 }
296 authMode = AuthModeSecret
297 deviceName = clientLabel
298
299 // Update last used timestamp in background
300 go func() {
301 if updateErr := b.config.Authorizer.UpdateNRCClientLastUsed(clientID); updateErr != nil {
302 log.W.F("failed to update NRC client last used: %v", updateErr)
303 }
304 }()
305 return
306 }
307 }
308
309 // Fallback to static map (for backwards compatibility)
310 if name, ok := b.config.AuthorizedSecrets[clientPubkeyHex]; ok {
311 // Secret auth uses ECDH between relay key and client's derived key
312 conversationKey, err = encryption.GenerateConversationKey(
313 b.config.Signer.Sec(),
314 clientPubkey,
315 )
316 if chk.E(err) {
317 return
318 }
319 authMode = AuthModeSecret
320 deviceName = name
321 return
322 }
323
324 err = ErrUnauthorized
325 return
326 }
327
328 // forwardToLocalRelay forwards a request to the local relay and handles responses.
329 func (b *Bridge) forwardToLocalRelay(ctx context.Context, session *Session, reqEvent *event.E, reqMsg *RequestMessage) error {
330 // Connect to local relay
331 localConn, err := ws.RelayConnect(ctx, b.config.LocalRelayURL)
332 if chk.E(err) {
333 return fmt.Errorf("%w: %v", ErrRelayConnectionFailed, err)
334 }
335 defer localConn.Close()
336
337 // Handle different message types
338 switch reqMsg.Type {
339 case "REQ":
340 return b.handleREQ(ctx, session, reqEvent, reqMsg, localConn)
341 case "EVENT":
342 return b.handleEVENT(ctx, session, reqEvent, reqMsg, localConn)
343 case "CLOSE":
344 return b.handleCLOSE(ctx, session, reqEvent, reqMsg)
345 case "COUNT":
346 return b.handleCOUNT(ctx, session, reqEvent, reqMsg, localConn)
347 case "IDS":
348 return b.handleIDS(ctx, session, reqEvent, reqMsg, localConn)
349 default:
350 return fmt.Errorf("unsupported message type: %s", reqMsg.Type)
351 }
352 }
353
354 // handleREQ handles a REQ message and forwards responses.
355 func (b *Bridge) handleREQ(ctx context.Context, session *Session, reqEvent *event.E, reqMsg *RequestMessage, conn *ws.Client) error {
356 // Extract subscription ID and filters from payload
357 // Payload: ["REQ", "<sub_id>", filter1, filter2, ...]
358 if len(reqMsg.Payload) < 3 {
359 return fmt.Errorf("invalid REQ payload")
360 }
361 subID, ok := reqMsg.Payload[1].(string)
362 if !ok {
363 return fmt.Errorf("invalid subscription ID")
364 }
365
366 // Parse filters from payload
367 var filters []*filter.F
368 for i := 2; i < len(reqMsg.Payload); i++ {
369 filterMap, ok := reqMsg.Payload[i].(map[string]any)
370 if !ok {
371 continue
372 }
373 filterBytes, err := json.Marshal(filterMap)
374 if err != nil {
375 continue
376 }
377 var f filter.F
378 if err := json.Unmarshal(filterBytes, &f); err != nil {
379 continue
380 }
381 filters = append(filters, &f)
382 }
383
384 if len(filters) == 0 {
385 return fmt.Errorf("no valid filters in REQ")
386 }
387
388 // Add subscription to session
389 if err := session.AddSubscription(subID); err != nil {
390 return err
391 }
392
393 // Create filter set
394 filterSet := filter.NewS(filters...)
395
396 // Subscribe to local relay
397 sub, err := conn.Subscribe(ctx, filterSet)
398 if chk.E(err) {
399 session.RemoveSubscription(subID)
400 return fmt.Errorf("local subscribe failed: %w", err)
401 }
402 defer sub.Unsub()
403
404 // Forward events until EOSE or timeout
405 for {
406 select {
407 case <-ctx.Done():
408 return ctx.Err()
409 case ev := <-sub.Events:
410 if ev == nil {
411 // Subscription closed, send EOSE
412 resp := &ResponseMessage{
413 Type: "EOSE",
414 Payload: []any{"EOSE", subID},
415 }
416 return b.sendResponse(ctx, reqEvent, session, resp)
417 }
418
419 // Convert event to JSON-compatible map
420 eventBytes, err := json.Marshal(ev)
421 if err != nil {
422 continue
423 }
424 var eventMap map[string]any
425 if err := json.Unmarshal(eventBytes, &eventMap); err != nil {
426 continue
427 }
428
429 // Send EVENT response
430 resp := &ResponseMessage{
431 Type: "EVENT",
432 Payload: []any{"EVENT", subID, eventMap},
433 }
434 if err := b.sendResponse(ctx, reqEvent, session, resp); err != nil {
435 log.W.F("failed to send event response: %v", err)
436 }
437 session.IncrementEventCount(subID)
438 case <-sub.EndOfStoredEvents:
439 // Send EOSE
440 session.MarkEOSE(subID)
441 resp := &ResponseMessage{
442 Type: "EOSE",
443 Payload: []any{"EOSE", subID},
444 }
445 return b.sendResponse(ctx, reqEvent, session, resp)
446 }
447 }
448 }
449
450 // handleEVENT handles an EVENT message and forwards the OK response.
451 func (b *Bridge) handleEVENT(ctx context.Context, session *Session, reqEvent *event.E, reqMsg *RequestMessage, conn *ws.Client) error {
452 // Extract event from payload: ["EVENT", {...event...}]
453 if len(reqMsg.Payload) < 2 {
454 return fmt.Errorf("invalid EVENT payload")
455 }
456
457 eventMap, ok := reqMsg.Payload[1].(map[string]any)
458 if !ok {
459 return fmt.Errorf("invalid event data")
460 }
461
462 // Parse event
463 eventBytes, err := json.Marshal(eventMap)
464 if err != nil {
465 return fmt.Errorf("failed to marshal event: %w", err)
466 }
467
468 var ev event.E
469 if err := json.Unmarshal(eventBytes, &ev); err != nil {
470 return fmt.Errorf("failed to unmarshal event: %w", err)
471 }
472
473 // Publish to local relay
474 err = conn.Publish(ctx, &ev)
475 success := err == nil
476 message := ""
477 if err != nil {
478 message = err.Error()
479 }
480
481 // Send OK response
482 resp := &ResponseMessage{
483 Type: "OK",
484 Payload: []any{"OK", string(hex.Enc(ev.ID[:])), success, message},
485 }
486 return b.sendResponse(ctx, reqEvent, session, resp)
487 }
488
489 // handleCLOSE handles a CLOSE message.
490 func (b *Bridge) handleCLOSE(ctx context.Context, session *Session, reqEvent *event.E, reqMsg *RequestMessage) error {
491 // Extract subscription ID: ["CLOSE", "<sub_id>"]
492 if len(reqMsg.Payload) >= 2 {
493 if subID, ok := reqMsg.Payload[1].(string); ok {
494 session.RemoveSubscription(subID)
495 }
496 }
497 // CLOSE doesn't have a response
498 return nil
499 }
500
501 // handleCOUNT handles a COUNT message.
502 func (b *Bridge) handleCOUNT(ctx context.Context, session *Session, reqEvent *event.E, reqMsg *RequestMessage, conn *ws.Client) error {
503 // COUNT is not supported via ws.Client directly, return error
504 resp := &ResponseMessage{
505 Type: "NOTICE",
506 Payload: []any{"NOTICE", "COUNT not supported through NRC tunnel"},
507 }
508 return b.sendResponse(ctx, reqEvent, session, resp)
509 }
510
511 // handleIDS handles an IDS message - returns event manifests for diffing.
512 func (b *Bridge) handleIDS(ctx context.Context, session *Session, reqEvent *event.E, reqMsg *RequestMessage, conn *ws.Client) error {
513 // Extract subscription ID and filters from payload
514 // Payload: ["IDS", "<sub_id>", filter1, filter2, ...]
515 if len(reqMsg.Payload) < 3 {
516 return fmt.Errorf("invalid IDS payload")
517 }
518 subID, ok := reqMsg.Payload[1].(string)
519 if !ok {
520 return fmt.Errorf("invalid subscription ID")
521 }
522
523 // Parse filters from payload
524 var filters []*filter.F
525 for i := 2; i < len(reqMsg.Payload); i++ {
526 filterMap, ok := reqMsg.Payload[i].(map[string]any)
527 if !ok {
528 continue
529 }
530 filterBytes, err := json.Marshal(filterMap)
531 if err != nil {
532 continue
533 }
534 var f filter.F
535 if err := json.Unmarshal(filterBytes, &f); err != nil {
536 continue
537 }
538 filters = append(filters, &f)
539 }
540
541 if len(filters) == 0 {
542 return fmt.Errorf("no valid filters in IDS")
543 }
544
545 // Add subscription to session
546 if err := session.AddSubscription(subID); err != nil {
547 return err
548 }
549 defer session.RemoveSubscription(subID)
550
551 // Create filter set
552 filterSet := filter.NewS(filters...)
553
554 // Subscribe to local relay
555 sub, err := conn.Subscribe(ctx, filterSet)
556 if chk.E(err) {
557 return fmt.Errorf("local subscribe failed: %w", err)
558 }
559 defer sub.Unsub()
560
561 // Collect events and build manifest
562 var manifest []EventManifestEntry
563 for {
564 select {
565 case <-ctx.Done():
566 return ctx.Err()
567 case ev := <-sub.Events:
568 if ev == nil {
569 // Subscription closed, send IDS response
570 return b.sendIDSResponse(ctx, reqEvent, session, subID, manifest)
571 }
572
573 // Build manifest entry
574 entry := EventManifestEntry{
575 Kind: int(ev.Kind),
576 ID: string(hex.Enc(ev.ID[:])),
577 CreatedAt: ev.CreatedAt,
578 }
579
580 // Check for d tag (parameterized replaceable events)
581 dTag := ev.Tags.GetFirst([]byte("d"))
582 if dTag != nil && dTag.Len() >= 2 {
583 entry.D = string(dTag.Value())
584 }
585
586 manifest = append(manifest, entry)
587 case <-sub.EndOfStoredEvents:
588 // Send IDS response with manifest
589 return b.sendIDSResponse(ctx, reqEvent, session, subID, manifest)
590 }
591 }
592 }
593
594 // sendIDSResponse sends an IDS response with the event manifest, chunking if necessary.
595 func (b *Bridge) sendIDSResponse(ctx context.Context, reqEvent *event.E, session *Session, subID string, manifest []EventManifestEntry) error {
596 resp := &ResponseMessage{
597 Type: "IDS",
598 Payload: []any{"IDS", subID, manifest},
599 }
600 return b.sendResponseChunked(ctx, reqEvent, session, resp)
601 }
602
603 // sendResponseChunked sends a response, chunking if necessary for large payloads.
604 func (b *Bridge) sendResponseChunked(ctx context.Context, reqEvent *event.E, session *Session, resp *ResponseMessage) error {
605 // Marshal response content
606 content, err := MarshalResponseContent(resp)
607 if err != nil {
608 return fmt.Errorf("marshal failed: %w", err)
609 }
610
611 // If small enough, send directly
612 if len(content) <= MaxChunkSize {
613 return b.sendResponse(ctx, reqEvent, session, resp)
614 }
615
616 // Need to chunk - encode to base64 for safe transmission
617 encoded := base64.StdEncoding.EncodeToString(content)
618 var chunks []string
619
620 // Split into chunks
621 for i := 0; i < len(encoded); i += MaxChunkSize {
622 end := i + MaxChunkSize
623 if end > len(encoded) {
624 end = len(encoded)
625 }
626 chunks = append(chunks, encoded[i:end])
627 }
628
629 // Generate message ID
630 messageID := generateMessageID()
631 log.D.F("NRC: chunking large message (%d bytes) into %d chunks", len(content), len(chunks))
632
633 // Send each chunk
634 for i, chunkData := range chunks {
635 chunkMsg := ChunkMessage{
636 Type: "CHUNK",
637 MessageID: messageID,
638 Index: i,
639 Total: len(chunks),
640 Data: chunkData,
641 }
642
643 chunkResp := &ResponseMessage{
644 Type: "CHUNK",
645 Payload: []any{chunkMsg},
646 }
647
648 if err := b.sendResponse(ctx, reqEvent, session, chunkResp); err != nil {
649 return fmt.Errorf("failed to send chunk %d/%d: %w", i+1, len(chunks), err)
650 }
651 }
652
653 return nil
654 }
655
656 // generateMessageID generates a random message ID for chunking.
657 func generateMessageID() string {
658 b := make([]byte, 16)
659 rand.Read(b)
660 return string(hex.Enc(b))
661 }
662
663 // sendResponse encrypts and sends a response to the client.
664 func (b *Bridge) sendResponse(ctx context.Context, reqEvent *event.E, session *Session, resp *ResponseMessage) error {
665 // Marshal response content
666 content, err := MarshalResponseContent(resp)
667 if err != nil {
668 return fmt.Errorf("marshal failed: %w", err)
669 }
670
671 // Encrypt content
672 encrypted, err := encryption.Encrypt(session.ConversationKey, content, nil)
673 if err != nil {
674 return fmt.Errorf("%w: %v", ErrEncryptionFailed, err)
675 }
676
677 // Build response event
678 respEvent := &event.E{
679 Content: []byte(encrypted),
680 CreatedAt: time.Now().Unix(),
681 Kind: KindNRCResponse,
682 Tags: tag.NewS(
683 tag.NewFromAny("p", hex.Enc(reqEvent.Pubkey[:])),
684 tag.NewFromAny("encryption", "nip44_v2"),
685 tag.NewFromAny("session", session.ID),
686 tag.NewFromAny("e", hex.Enc(reqEvent.ID[:])),
687 ),
688 }
689
690 // Sign with relay key
691 if err := respEvent.Sign(b.config.Signer); chk.E(err) {
692 return fmt.Errorf("signing failed: %w", err)
693 }
694
695 // Publish to rendezvous relay
696 b.mu.RLock()
697 conn := b.rendezvousConn
698 b.mu.RUnlock()
699
700 if conn == nil {
701 return fmt.Errorf("not connected to rendezvous relay")
702 }
703
704 if err := conn.Publish(ctx, respEvent); chk.E(err) {
705 return fmt.Errorf("publish failed: %w", err)
706 }
707
708 return nil
709 }
710
711 // sendError sends an error response to the client.
712 func (b *Bridge) sendError(ctx context.Context, reqEvent *event.E, sessionID string, errMsg string) {
713 // For errors, we need to get or create a conversation key
714 // This is best-effort since we may not be able to authenticate
715 conversationKey, err := encryption.GenerateConversationKey(
716 b.config.Signer.Sec(),
717 reqEvent.Pubkey[:],
718 )
719 if err != nil {
720 log.W.F("failed to generate conversation key for error response: %v", err)
721 return
722 }
723
724 resp := &ResponseMessage{
725 Type: "NOTICE",
726 Payload: []any{"NOTICE", "nrc: " + errMsg},
727 }
728
729 content, err := MarshalResponseContent(resp)
730 if err != nil {
731 return
732 }
733
734 encrypted, err := encryption.Encrypt(conversationKey, content, nil)
735 if err != nil {
736 return
737 }
738
739 respEvent := &event.E{
740 Content: []byte(encrypted),
741 CreatedAt: time.Now().Unix(),
742 Kind: KindNRCResponse,
743 Tags: tag.NewS(
744 tag.NewFromAny("p", hex.Enc(reqEvent.Pubkey[:])),
745 tag.NewFromAny("encryption", "nip44_v2"),
746 tag.NewFromAny("session", sessionID),
747 tag.NewFromAny("e", hex.Enc(reqEvent.ID[:])),
748 ),
749 }
750
751 if err := respEvent.Sign(b.config.Signer); err != nil {
752 return
753 }
754
755 b.mu.RLock()
756 conn := b.rendezvousConn
757 b.mu.RUnlock()
758
759 if conn != nil {
760 conn.Publish(ctx, respEvent)
761 }
762 }
763
764 // AddAuthorizedSecret adds an authorized secret (derived pubkey).
765 func (b *Bridge) AddAuthorizedSecret(pubkeyHex, deviceName string) {
766 b.config.AuthorizedSecrets[pubkeyHex] = deviceName
767 }
768
769 // RemoveAuthorizedSecret removes an authorized secret.
770 func (b *Bridge) RemoveAuthorizedSecret(pubkeyHex string) {
771 delete(b.config.AuthorizedSecrets, pubkeyHex)
772 }
773
774 // ListAuthorizedSecrets returns a copy of the authorized secrets map.
775 func (b *Bridge) ListAuthorizedSecrets() map[string]string {
776 result := make(map[string]string)
777 for k, v := range b.config.AuthorizedSecrets {
778 result[k] = v
779 }
780 return result
781 }
782
783 // SessionCount returns the number of active sessions.
784 func (b *Bridge) SessionCount() int {
785 return b.sessions.Count()
786 }
787