| 1 | package records |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "fmt" |
| 8 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 9 | "net/url" |
| 10 | "strings" |
| 11 | "time" |
| 12 | ) |
| 13 | |
| 14 | // GenerateEphemeral signs a relay-owned response without invoking the |
| 15 | // durable records callback. It is used for protocol responses such as the |
| 16 | // NIP-43 invite request, which must be relay-signed but never stored. |
| 17 | func (s *Service) GenerateEphemeral(ctx context.Context, kind int, tags [][]string, content string, now int64) (event.Event, error) { |
| 18 | if err := ctx.Err(); err != nil { |
| 19 | return event.Event{}, err |
| 20 | } |
| 21 | if now == 0 { |
| 22 | now = time.Now().Unix() |
| 23 | } |
| 24 | // Protocol proofs use wall-clock time. The immutable relay key is safe to |
| 25 | // read concurrently; taking the projection lock here would invert the |
| 26 | // router/records lock order for responses generated inside a REQ. |
| 27 | out := event.Event{CreatedAt: now, Kind: kind, Tags: tags, Content: content} |
| 28 | if err := event.Sign(&out, s.secret); err != nil { |
| 29 | return event.Event{}, err |
| 30 | } |
| 31 | return out, nil |
| 32 | } |
| 33 | |
| 34 | // SignNIP98 creates a short-lived HTTP authorization proof with the durable |
| 35 | // relay identity. The caller supplies the exact URL Git will request and the |
| 36 | // body hash when the method has a payload. |
| 37 | func (s *Service) SignNIP98(ctx context.Context, method, rawURL, payloadHash string, now int64) (event.Event, error) { |
| 38 | if _, err := url.ParseRequestURI(rawURL); err != nil || !strings.Contains(rawURL, "://" ) { |
| 39 | return event.Event{}, fmt.Errorf("records: invalid NIP-98 URL: %w" , err) |
| 40 | } |
| 41 | secret, err := event.GenerateKey() |
| 42 | if err != nil { |
| 43 | return event.Event{}, err |
| 44 | } |
| 45 | tags := [][]string{{"u" , rawURL}, {"method" , strings.ToUpper(method)}, {"nonce" , secret}} |
| 46 | if payloadHash != "" { |
| 47 | if len(payloadHash) != sha256.Size*2 { |
| 48 | return event.Event{}, fmt.Errorf("records: invalid NIP-98 payload hash" ) |
| 49 | } |
| 50 | if _, err := hex.DecodeString(payloadHash); err != nil { |
| 51 | return event.Event{}, fmt.Errorf("records: invalid NIP-98 payload hash: %w" , err) |
| 52 | } |
| 53 | tags = append(tags, []string{"payload" , payloadHash}) |
| 54 | } |
| 55 | if now == 0 { |
| 56 | now = time.Now().Unix() |
| 57 | } |
| 58 | return s.GenerateEphemeral(ctx, 27235, tags, "" , now) |
| 59 | } |
| 60 | |
| 61 | // SignNIP42 answers a relay challenge without persisting an AUTH event. |
| 62 | func (s *Service) SignNIP42(ctx context.Context, relayURL, challenge string, now int64) (event.Event, error) { |
| 63 | if relayURL == "" || challenge == "" { |
| 64 | return event.Event{}, fmt.Errorf("records: missing NIP-42 challenge" ) |
| 65 | } |
| 66 | return s.GenerateEphemeral(ctx, event.KIND_AUTH, [][]string{{"relay" , relayURL}, {"challenge" , challenge}}, "" , now) |
| 67 | } |