nip43.go raw
1 //go:build js && wasm
2
3 package wasmdb
4
5 import (
6 "bytes"
7 "encoding/binary"
8 "errors"
9 "time"
10
11 "github.com/aperturerobotics/go-indexeddb/idb"
12 "github.com/hack-pad/safejs"
13
14 "next.orly.dev/pkg/database"
15 )
16
17 const (
18 // NIP43StoreName is the object store for NIP-43 membership
19 NIP43StoreName = "nip43"
20
21 // InvitesStoreName is the object store for invite codes
22 InvitesStoreName = "invites"
23 )
24
25 // AddNIP43Member adds a pubkey as a NIP-43 member with the given invite code
26 func (w *W) AddNIP43Member(pubkey []byte, inviteCode string) error {
27 if len(pubkey) != 32 {
28 return errors.New("invalid pubkey length")
29 }
30
31 // Create membership record
32 membership := &database.NIP43Membership{
33 Pubkey: make([]byte, 32),
34 InviteCode: inviteCode,
35 AddedAt: time.Now(),
36 }
37 copy(membership.Pubkey, pubkey)
38
39 // Serialize membership
40 data := w.serializeNIP43Membership(membership)
41
42 // Store using pubkey as key
43 return w.setStoreValue(NIP43StoreName, string(pubkey), data)
44 }
45
46 // RemoveNIP43Member removes a pubkey from NIP-43 membership
47 func (w *W) RemoveNIP43Member(pubkey []byte) error {
48 return w.deleteStoreValue(NIP43StoreName, string(pubkey))
49 }
50
51 // IsNIP43Member checks if a pubkey is a NIP-43 member
52 func (w *W) IsNIP43Member(pubkey []byte) (isMember bool, err error) {
53 data, err := w.getStoreValue(NIP43StoreName, string(pubkey))
54 if err != nil {
55 return false, nil // Not found is not an error, just not a member
56 }
57 return data != nil, nil
58 }
59
60 // GetNIP43Membership returns the full membership details for a pubkey
61 func (w *W) GetNIP43Membership(pubkey []byte) (*database.NIP43Membership, error) {
62 data, err := w.getStoreValue(NIP43StoreName, string(pubkey))
63 if err != nil {
64 return nil, err
65 }
66 if data == nil {
67 return nil, errors.New("membership not found")
68 }
69
70 return w.deserializeNIP43Membership(data)
71 }
72
73 // GetAllNIP43Members returns all NIP-43 member pubkeys
74 func (w *W) GetAllNIP43Members() ([][]byte, error) {
75 tx, err := w.db.Transaction(idb.TransactionReadOnly, NIP43StoreName)
76 if err != nil {
77 return nil, err
78 }
79
80 store, err := tx.ObjectStore(NIP43StoreName)
81 if err != nil {
82 return nil, err
83 }
84
85 var members [][]byte
86
87 cursorReq, err := store.OpenCursor(idb.CursorNext)
88 if err != nil {
89 return nil, err
90 }
91
92 err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
93 keyVal, keyErr := cursor.Key()
94 if keyErr != nil {
95 return keyErr
96 }
97
98 // Key is the pubkey stored as string
99 keyBytes := safeValueToBytes(keyVal)
100 if len(keyBytes) == 32 {
101 pubkey := make([]byte, 32)
102 copy(pubkey, keyBytes)
103 members = append(members, pubkey)
104 }
105
106 return cursor.Continue()
107 })
108
109 if err != nil && err.Error() != "found" {
110 return nil, err
111 }
112
113 return members, nil
114 }
115
116 // StoreInviteCode stores an invite code with expiration time
117 func (w *W) StoreInviteCode(code string, expiresAt time.Time) error {
118 // Serialize expiration time as unix timestamp
119 data := make([]byte, 8)
120 binary.BigEndian.PutUint64(data, uint64(expiresAt.Unix()))
121
122 return w.setStoreValue(InvitesStoreName, code, data)
123 }
124
125 // ValidateInviteCode checks if an invite code is valid (exists and not expired)
126 func (w *W) ValidateInviteCode(code string) (valid bool, err error) {
127 data, err := w.getStoreValue(InvitesStoreName, code)
128 if err != nil {
129 return false, nil
130 }
131 if data == nil || len(data) < 8 {
132 return false, nil
133 }
134
135 // Check expiration
136 expiresAt := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)
137 if time.Now().After(expiresAt) {
138 return false, nil
139 }
140
141 return true, nil
142 }
143
144 // DeleteInviteCode removes an invite code
145 func (w *W) DeleteInviteCode(code string) error {
146 return w.deleteStoreValue(InvitesStoreName, code)
147 }
148
149 // PublishNIP43MembershipEvent is a no-op in WASM (events are handled by the relay)
150 func (w *W) PublishNIP43MembershipEvent(kind int, pubkey []byte) error {
151 // In WASM context, this would typically be handled by the client
152 // This is a no-op implementation
153 return nil
154 }
155
156 // serializeNIP43Membership converts a membership to bytes for storage
157 func (w *W) serializeNIP43Membership(m *database.NIP43Membership) []byte {
158 buf := new(bytes.Buffer)
159
160 // Write pubkey (32 bytes)
161 buf.Write(m.Pubkey)
162
163 // Write AddedAt as unix timestamp (8 bytes)
164 ts := make([]byte, 8)
165 binary.BigEndian.PutUint64(ts, uint64(m.AddedAt.Unix()))
166 buf.Write(ts)
167
168 // Write invite code length (4 bytes) + invite code
169 codeBytes := []byte(m.InviteCode)
170 codeLen := make([]byte, 4)
171 binary.BigEndian.PutUint32(codeLen, uint32(len(codeBytes)))
172 buf.Write(codeLen)
173 buf.Write(codeBytes)
174
175 return buf.Bytes()
176 }
177
178 // deserializeNIP43Membership converts bytes back to a membership
179 func (w *W) deserializeNIP43Membership(data []byte) (*database.NIP43Membership, error) {
180 if len(data) < 44 { // 32 + 8 + 4 minimum
181 return nil, errors.New("invalid membership data")
182 }
183
184 m := &database.NIP43Membership{}
185
186 // Read pubkey
187 m.Pubkey = make([]byte, 32)
188 copy(m.Pubkey, data[:32])
189
190 // Read AddedAt
191 m.AddedAt = time.Unix(int64(binary.BigEndian.Uint64(data[32:40])), 0)
192
193 // Read invite code
194 codeLen := binary.BigEndian.Uint32(data[40:44])
195 if len(data) < int(44+codeLen) {
196 return nil, errors.New("invalid invite code length")
197 }
198 m.InviteCode = string(data[44 : 44+codeLen])
199
200 return m, nil
201 }
202
203 // Helper to convert safejs.Value to string for keys
204 func safeValueToString(v safejs.Value) string {
205 if v.IsUndefined() || v.IsNull() {
206 return ""
207 }
208 str, err := v.String()
209 if err != nil {
210 return ""
211 }
212 return str
213 }
214