types.go raw
1 package nip43
2
3 import (
4 "crypto/rand"
5 "encoding/base64"
6 "sync"
7 "time"
8
9 "next.orly.dev/pkg/nostr/encoders/event"
10 "next.orly.dev/pkg/nostr/encoders/hex"
11 "next.orly.dev/pkg/nostr/encoders/tag"
12 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
13 )
14
15 // Event kinds defined by NIP-43
16 const (
17 KindMemberList = 13534 // Membership list published by relay
18 KindAddUser = 8000 // Add user event published by relay
19 KindRemoveUser = 8001 // Remove user event published by relay
20 KindJoinRequest = 28934 // Join request sent by user
21 KindInviteReq = 28935 // Invite request (ephemeral)
22 KindLeaveRequest = 28936 // Leave request sent by user
23 )
24
25 // InviteCode represents a claim/invite code for relay access
26 type InviteCode struct {
27 Code string
28 ExpiresAt time.Time
29 UsedBy []byte // pubkey that used this code, nil if unused
30 CreatedAt time.Time
31 }
32
33 // InviteManager manages invite codes for NIP-43
34 type InviteManager struct {
35 mu sync.RWMutex
36 codes map[string]*InviteCode
37 expiry time.Duration
38 }
39
40 // NewInviteManager creates a new invite code manager
41 func NewInviteManager(expiryDuration time.Duration) *InviteManager {
42 if expiryDuration == 0 {
43 expiryDuration = 24 * time.Hour // Default: 24 hours
44 }
45 return &InviteManager{
46 codes: make(map[string]*InviteCode),
47 expiry: expiryDuration,
48 }
49 }
50
51 // GenerateCode creates a new invite code
52 func (im *InviteManager) GenerateCode() (code string, err error) {
53 // Generate 32 random bytes
54 b := make([]byte, 32)
55 if _, err = rand.Read(b); err != nil {
56 return
57 }
58 code = base64.URLEncoding.EncodeToString(b)
59
60 im.mu.Lock()
61 defer im.mu.Unlock()
62
63 im.codes[code] = &InviteCode{
64 Code: code,
65 CreatedAt: time.Now(),
66 ExpiresAt: time.Now().Add(im.expiry),
67 }
68
69 return code, nil
70 }
71
72 // ValidateAndConsume validates an invite code and marks it as used by the given pubkey
73 func (im *InviteManager) ValidateAndConsume(code string, pubkey []byte) (valid bool, reason string) {
74 im.mu.Lock()
75 defer im.mu.Unlock()
76
77 invite, exists := im.codes[code]
78 if !exists {
79 return false, "invalid invite code"
80 }
81
82 if time.Now().After(invite.ExpiresAt) {
83 delete(im.codes, code)
84 return false, "invite code expired"
85 }
86
87 if invite.UsedBy != nil {
88 return false, "invite code already used"
89 }
90
91 // Mark as used
92 invite.UsedBy = make([]byte, len(pubkey))
93 copy(invite.UsedBy, pubkey)
94
95 return true, ""
96 }
97
98 // CleanupExpired removes expired invite codes
99 func (im *InviteManager) CleanupExpired() {
100 im.mu.Lock()
101 defer im.mu.Unlock()
102
103 now := time.Now()
104 for code, invite := range im.codes {
105 if now.After(invite.ExpiresAt) {
106 delete(im.codes, code)
107 }
108 }
109 }
110
111 // BuildMemberListEvent creates a kind 13534 membership list event
112 // relaySecretKey: the relay's identity secret key (32 bytes)
113 // members: list of member pubkeys (32 bytes each)
114 func BuildMemberListEvent(relaySecretKey []byte, members [][]byte) (*event.E, error) {
115 // Create signer
116 signer, err := p8k.New()
117 if err != nil {
118 return nil, err
119 }
120 if err = signer.InitSec(relaySecretKey); err != nil {
121 return nil, err
122 }
123
124 ev := event.New()
125 ev.Kind = KindMemberList
126 copy(ev.Pubkey, signer.Pub())
127
128 // Initialize tags
129 ev.Tags = tag.NewS()
130
131 // Add NIP-70 `-` tag
132 ev.Tags.Append(tag.NewFromAny("-"))
133
134 // Add member tags
135 for _, member := range members {
136 if len(member) == 32 {
137 ev.Tags.Append(tag.NewFromAny("member", hex.Enc(member)))
138 }
139 }
140
141 ev.CreatedAt = time.Now().Unix()
142 ev.Content = []byte("")
143
144 // Sign the event
145 if err := ev.Sign(signer); err != nil {
146 return nil, err
147 }
148
149 return ev, nil
150 }
151
152 // BuildAddUserEvent creates a kind 8000 add user event
153 func BuildAddUserEvent(relaySecretKey []byte, userPubkey []byte) (*event.E, error) {
154 // Create signer
155 signer, err := p8k.New()
156 if err != nil {
157 return nil, err
158 }
159 if err = signer.InitSec(relaySecretKey); err != nil {
160 return nil, err
161 }
162
163 ev := event.New()
164 ev.Kind = KindAddUser
165 copy(ev.Pubkey, signer.Pub())
166
167 // Initialize tags
168 ev.Tags = tag.NewS()
169
170 // Add NIP-70 `-` tag
171 ev.Tags.Append(tag.NewFromAny("-"))
172
173 // Add p tag for the user
174 if len(userPubkey) == 32 {
175 ev.Tags.Append(tag.NewFromAny("p", hex.Enc(userPubkey)))
176 }
177
178 ev.CreatedAt = time.Now().Unix()
179 ev.Content = []byte("")
180
181 // Sign the event
182 if err := ev.Sign(signer); err != nil {
183 return nil, err
184 }
185
186 return ev, nil
187 }
188
189 // BuildRemoveUserEvent creates a kind 8001 remove user event
190 func BuildRemoveUserEvent(relaySecretKey []byte, userPubkey []byte) (*event.E, error) {
191 // Create signer
192 signer, err := p8k.New()
193 if err != nil {
194 return nil, err
195 }
196 if err = signer.InitSec(relaySecretKey); err != nil {
197 return nil, err
198 }
199
200 ev := event.New()
201 ev.Kind = KindRemoveUser
202 copy(ev.Pubkey, signer.Pub())
203
204 // Initialize tags
205 ev.Tags = tag.NewS()
206
207 // Add NIP-70 `-` tag
208 ev.Tags.Append(tag.NewFromAny("-"))
209
210 // Add p tag for the user
211 if len(userPubkey) == 32 {
212 ev.Tags.Append(tag.NewFromAny("p", hex.Enc(userPubkey)))
213 }
214
215 ev.CreatedAt = time.Now().Unix()
216 ev.Content = []byte("")
217
218 // Sign the event
219 if err := ev.Sign(signer); err != nil {
220 return nil, err
221 }
222
223 return ev, nil
224 }
225
226 // BuildInviteEvent creates a kind 28935 invite event (ephemeral)
227 func BuildInviteEvent(relaySecretKey []byte, inviteCode string) (*event.E, error) {
228 // Create signer
229 signer, err := p8k.New()
230 if err != nil {
231 return nil, err
232 }
233 if err = signer.InitSec(relaySecretKey); err != nil {
234 return nil, err
235 }
236
237 ev := event.New()
238 ev.Kind = KindInviteReq
239 copy(ev.Pubkey, signer.Pub())
240
241 // Initialize tags
242 ev.Tags = tag.NewS()
243
244 // Add NIP-70 `-` tag
245 ev.Tags.Append(tag.NewFromAny("-"))
246
247 // Add claim tag
248 ev.Tags.Append(tag.NewFromAny("claim", inviteCode))
249
250 ev.CreatedAt = time.Now().Unix()
251 ev.Content = []byte("")
252
253 // Sign the event
254 if err := ev.Sign(signer); err != nil {
255 return nil, err
256 }
257
258 return ev, nil
259 }
260
261 // ValidateJoinRequest validates a kind 28934 join request event
262 func ValidateJoinRequest(ev *event.E) (inviteCode string, valid bool, reason string) {
263 // Must be kind 28934
264 if ev.Kind != KindJoinRequest {
265 return "", false, "invalid event kind"
266 }
267
268 // Must have NIP-70 `-` tag
269 hasMinusTag := ev.Tags.GetFirst([]byte("-")) != nil
270 if !hasMinusTag {
271 return "", false, "missing NIP-70 `-` tag"
272 }
273
274 // Must have claim tag
275 claimTag := ev.Tags.GetFirst([]byte("claim"))
276 if claimTag != nil && claimTag.Len() >= 2 {
277 inviteCode = string(claimTag.T[1])
278 }
279 if inviteCode == "" {
280 return "", false, "missing claim tag"
281 }
282
283 // Check timestamp (must be recent, within +/- 10 minutes)
284 now := time.Now().Unix()
285 if ev.CreatedAt < now-600 || ev.CreatedAt > now+600 {
286 return inviteCode, false, "timestamp out of range"
287 }
288
289 return inviteCode, true, ""
290 }
291
292 // ValidateLeaveRequest validates a kind 28936 leave request event
293 func ValidateLeaveRequest(ev *event.E) (valid bool, reason string) {
294 // Must be kind 28936
295 if ev.Kind != KindLeaveRequest {
296 return false, "invalid event kind"
297 }
298
299 // Must have NIP-70 `-` tag
300 hasMinusTag := ev.Tags.GetFirst([]byte("-")) != nil
301 if !hasMinusTag {
302 return false, "missing NIP-70 `-` tag"
303 }
304
305 // Check timestamp (must be recent, within +/- 10 minutes)
306 now := time.Now().Unix()
307 if ev.CreatedAt < now-600 || ev.CreatedAt > now+600 {
308 return false, "timestamp out of range"
309 }
310
311 return true, ""
312 }
313