Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

Compare changes

Choose any two refs to compare.

+16 -1
backend/go.mod
··· 3 3 go 1.24.0 4 4 5 5 require ( 6 + github.com/fxamacker/cbor/v2 v2.9.0 6 7 github.com/go-chi/chi/v5 v5.1.0 7 8 github.com/go-chi/cors v1.2.1 8 9 github.com/go-jose/go-jose/v4 v4.0.4 10 + github.com/gorilla/websocket v1.5.3 11 + github.com/ipfs/go-cid v0.6.0 9 12 github.com/joho/godotenv v1.5.1 10 13 github.com/lib/pq v1.10.9 11 14 github.com/mattn/go-sqlite3 v1.14.22 ··· 14 17 15 18 require ( 16 19 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 + github.com/klauspost/cpuid/v2 v2.0.9 // indirect 21 + github.com/minio/sha256-simd v1.0.0 // indirect 22 + github.com/mr-tron/base58 v1.2.0 // indirect 23 + github.com/multiformats/go-base32 v0.0.3 // indirect 24 + github.com/multiformats/go-base36 v0.1.0 // indirect 25 + github.com/multiformats/go-multibase v0.2.0 // indirect 26 + github.com/multiformats/go-multihash v0.2.3 // indirect 27 + github.com/multiformats/go-varint v0.1.0 // indirect 17 28 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 29 + github.com/spaolacci/murmur3 v1.1.0 // indirect 18 30 github.com/stretchr/testify v1.10.0 // indirect 19 - golang.org/x/crypto v0.31.0 // indirect 31 + github.com/x448/float16 v0.8.4 // indirect 32 + golang.org/x/crypto v0.35.0 // indirect 33 + golang.org/x/sys v0.30.0 // indirect 20 34 golang.org/x/text v0.32.0 // indirect 35 + lukechampine.com/blake3 v1.1.6 // indirect 21 36 )
+33 -2
backend/go.sum
··· 1 1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 2 2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 + github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 4 + github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 3 5 github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 4 6 github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 5 7 github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= ··· 8 10 github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 9 11 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 12 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 + github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 14 + github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 + github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= 16 + github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= 11 17 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 12 18 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 19 + github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 20 + github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 21 + github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 13 22 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 14 23 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 15 24 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 16 25 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 26 + github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= 27 + github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= 28 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 29 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 30 + github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= 31 + github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= 32 + github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= 33 + github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= 34 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 35 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 36 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 37 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 38 + github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= 39 + github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= 17 40 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 18 41 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 43 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 19 44 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 45 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 - golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 22 - golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 46 + github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 47 + github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 48 + golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 49 + golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 23 50 golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= 24 51 golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= 52 + golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 53 + golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 25 54 golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 26 55 golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 27 56 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 57 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 + lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= 59 + lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
+4 -1
backend/internal/api/annotations.go
··· 480 480 481 481 func resolveDIDToPDS(did string) (string, error) { 482 482 if strings.HasPrefix(did, "did:plc:") { 483 - resp, err := http.Get("https://plc.directory/" + did) 483 + client := &http.Client{ 484 + Timeout: 10 * time.Second, 485 + } 486 + resp, err := client.Get("https://plc.directory/" + did) 484 487 if err != nil { 485 488 return "", err 486 489 }
+435
backend/internal/api/apikey.go
··· 1 + package api 2 + 3 + import ( 4 + "crypto/rand" 5 + "crypto/sha256" 6 + "crypto/x509" 7 + "encoding/hex" 8 + "encoding/json" 9 + "encoding/pem" 10 + "fmt" 11 + "net/http" 12 + "strings" 13 + "time" 14 + 15 + "github.com/go-chi/chi/v5" 16 + 17 + "margin.at/internal/db" 18 + "margin.at/internal/xrpc" 19 + ) 20 + 21 + type APIKeyHandler struct { 22 + db *db.DB 23 + refresher *TokenRefresher 24 + } 25 + 26 + func NewAPIKeyHandler(database *db.DB, refresher *TokenRefresher) *APIKeyHandler { 27 + return &APIKeyHandler{db: database, refresher: refresher} 28 + } 29 + 30 + type CreateKeyRequest struct { 31 + Name string `json:"name"` 32 + } 33 + 34 + type CreateKeyResponse struct { 35 + ID string `json:"id"` 36 + Name string `json:"name"` 37 + Key string `json:"key"` 38 + CreatedAt time.Time `json:"createdAt"` 39 + } 40 + 41 + func (h *APIKeyHandler) CreateKey(w http.ResponseWriter, r *http.Request) { 42 + session, err := h.refresher.GetSessionWithAutoRefresh(r) 43 + if err != nil { 44 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 45 + return 46 + } 47 + 48 + var req CreateKeyRequest 49 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 50 + http.Error(w, "Invalid request body", http.StatusBadRequest) 51 + return 52 + } 53 + 54 + if req.Name == "" { 55 + req.Name = "API Key" 56 + } 57 + 58 + rawKey := generateAPIKey() 59 + keyHash := hashAPIKey(rawKey) 60 + keyID := generateKeyID() 61 + 62 + apiKey := &db.APIKey{ 63 + ID: keyID, 64 + OwnerDID: session.DID, 65 + Name: req.Name, 66 + KeyHash: keyHash, 67 + CreatedAt: time.Now(), 68 + } 69 + 70 + if err := h.db.CreateAPIKey(apiKey); err != nil { 71 + http.Error(w, "Failed to create key", http.StatusInternalServerError) 72 + return 73 + } 74 + 75 + w.Header().Set("Content-Type", "application/json") 76 + json.NewEncoder(w).Encode(CreateKeyResponse{ 77 + ID: keyID, 78 + Name: req.Name, 79 + Key: rawKey, 80 + CreatedAt: apiKey.CreatedAt, 81 + }) 82 + } 83 + 84 + func (h *APIKeyHandler) ListKeys(w http.ResponseWriter, r *http.Request) { 85 + session, err := h.refresher.GetSessionWithAutoRefresh(r) 86 + if err != nil { 87 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 88 + return 89 + } 90 + 91 + keys, err := h.db.GetAPIKeysByOwner(session.DID) 92 + if err != nil { 93 + http.Error(w, "Failed to get keys", http.StatusInternalServerError) 94 + return 95 + } 96 + 97 + if keys == nil { 98 + keys = []db.APIKey{} 99 + } 100 + 101 + w.Header().Set("Content-Type", "application/json") 102 + json.NewEncoder(w).Encode(map[string]interface{}{"keys": keys}) 103 + } 104 + 105 + func (h *APIKeyHandler) DeleteKey(w http.ResponseWriter, r *http.Request) { 106 + session, err := h.refresher.GetSessionWithAutoRefresh(r) 107 + if err != nil { 108 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 109 + return 110 + } 111 + 112 + keyID := chi.URLParam(r, "id") 113 + if keyID == "" { 114 + http.Error(w, "Key ID required", http.StatusBadRequest) 115 + return 116 + } 117 + 118 + if err := h.db.DeleteAPIKey(keyID, session.DID); err != nil { 119 + http.Error(w, "Failed to delete key", http.StatusInternalServerError) 120 + return 121 + } 122 + 123 + w.Header().Set("Content-Type", "application/json") 124 + json.NewEncoder(w).Encode(map[string]bool{"success": true}) 125 + } 126 + 127 + type QuickBookmarkRequest struct { 128 + URL string `json:"url"` 129 + Title string `json:"title,omitempty"` 130 + Description string `json:"description,omitempty"` 131 + } 132 + 133 + func (h *APIKeyHandler) QuickBookmark(w http.ResponseWriter, r *http.Request) { 134 + apiKey, err := h.authenticateAPIKey(r) 135 + if err != nil { 136 + http.Error(w, err.Error(), http.StatusUnauthorized) 137 + return 138 + } 139 + 140 + var req QuickBookmarkRequest 141 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 142 + http.Error(w, "Invalid request body", http.StatusBadRequest) 143 + return 144 + } 145 + 146 + if req.URL == "" { 147 + http.Error(w, "URL is required", http.StatusBadRequest) 148 + return 149 + } 150 + 151 + session, err := h.getSessionByDID(apiKey.OwnerDID) 152 + if err != nil { 153 + http.Error(w, "User session not found. Please log in to margin.at first.", http.StatusUnauthorized) 154 + return 155 + } 156 + 157 + urlHash := db.HashURL(req.URL) 158 + record := xrpc.NewBookmarkRecord(req.URL, urlHash, req.Title, req.Description) 159 + 160 + var result *xrpc.CreateRecordOutput 161 + err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 162 + var createErr error 163 + result, createErr = client.CreateRecord(r.Context(), did, xrpc.CollectionBookmark, record) 164 + return createErr 165 + }) 166 + if err != nil { 167 + http.Error(w, "Failed to create bookmark: "+err.Error(), http.StatusInternalServerError) 168 + return 169 + } 170 + 171 + h.db.UpdateAPIKeyLastUsed(apiKey.ID) 172 + 173 + var titlePtr, descPtr *string 174 + if req.Title != "" { 175 + titlePtr = &req.Title 176 + } 177 + if req.Description != "" { 178 + descPtr = &req.Description 179 + } 180 + 181 + cid := result.CID 182 + bookmark := &db.Bookmark{ 183 + URI: result.URI, 184 + AuthorDID: apiKey.OwnerDID, 185 + Source: req.URL, 186 + SourceHash: urlHash, 187 + Title: titlePtr, 188 + Description: descPtr, 189 + CreatedAt: time.Now(), 190 + IndexedAt: time.Now(), 191 + CID: &cid, 192 + } 193 + h.db.CreateBookmark(bookmark) 194 + 195 + w.Header().Set("Content-Type", "application/json") 196 + json.NewEncoder(w).Encode(map[string]string{ 197 + "uri": result.URI, 198 + "cid": result.CID, 199 + "message": "Bookmark created successfully", 200 + }) 201 + } 202 + 203 + type QuickAnnotationRequest struct { 204 + URL string `json:"url"` 205 + Text string `json:"text"` 206 + } 207 + 208 + func (h *APIKeyHandler) QuickAnnotation(w http.ResponseWriter, r *http.Request) { 209 + apiKey, err := h.authenticateAPIKey(r) 210 + if err != nil { 211 + http.Error(w, err.Error(), http.StatusUnauthorized) 212 + return 213 + } 214 + 215 + var req QuickAnnotationRequest 216 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 217 + http.Error(w, "Invalid request body", http.StatusBadRequest) 218 + return 219 + } 220 + 221 + if req.URL == "" || req.Text == "" { 222 + http.Error(w, "URL and text are required", http.StatusBadRequest) 223 + return 224 + } 225 + 226 + session, err := h.getSessionByDID(apiKey.OwnerDID) 227 + if err != nil { 228 + http.Error(w, "User session not found. Please log in to margin.at first.", http.StatusUnauthorized) 229 + return 230 + } 231 + 232 + urlHash := db.HashURL(req.URL) 233 + record := xrpc.NewAnnotationRecord(req.URL, urlHash, req.Text, nil, "") 234 + 235 + var result *xrpc.CreateRecordOutput 236 + err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 237 + var createErr error 238 + result, createErr = client.CreateRecord(r.Context(), did, xrpc.CollectionAnnotation, record) 239 + return createErr 240 + }) 241 + if err != nil { 242 + http.Error(w, "Failed to create annotation: "+err.Error(), http.StatusInternalServerError) 243 + return 244 + } 245 + 246 + h.db.UpdateAPIKeyLastUsed(apiKey.ID) 247 + 248 + bodyValue := req.Text 249 + annotation := &db.Annotation{ 250 + URI: result.URI, 251 + AuthorDID: apiKey.OwnerDID, 252 + Motivation: "commenting", 253 + BodyValue: &bodyValue, 254 + TargetSource: req.URL, 255 + TargetHash: urlHash, 256 + CreatedAt: time.Now(), 257 + IndexedAt: time.Now(), 258 + CID: &result.CID, 259 + } 260 + h.db.CreateAnnotation(annotation) 261 + 262 + w.Header().Set("Content-Type", "application/json") 263 + json.NewEncoder(w).Encode(map[string]string{ 264 + "uri": result.URI, 265 + "cid": result.CID, 266 + "message": "Annotation created successfully", 267 + }) 268 + } 269 + 270 + type QuickHighlightRequest struct { 271 + URL string `json:"url"` 272 + Selector interface{} `json:"selector"` 273 + Color string `json:"color,omitempty"` 274 + } 275 + 276 + func (h *APIKeyHandler) QuickHighlight(w http.ResponseWriter, r *http.Request) { 277 + apiKey, err := h.authenticateAPIKey(r) 278 + if err != nil { 279 + http.Error(w, err.Error(), http.StatusUnauthorized) 280 + return 281 + } 282 + 283 + var req QuickHighlightRequest 284 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 285 + http.Error(w, "Invalid request body", http.StatusBadRequest) 286 + return 287 + } 288 + 289 + if req.URL == "" || req.Selector == nil { 290 + http.Error(w, "URL and selector are required", http.StatusBadRequest) 291 + return 292 + } 293 + 294 + session, err := h.getSessionByDID(apiKey.OwnerDID) 295 + if err != nil { 296 + http.Error(w, "User session not found. Please log in to margin.at first.", http.StatusUnauthorized) 297 + return 298 + } 299 + 300 + urlHash := db.HashURL(req.URL) 301 + color := req.Color 302 + if color == "" { 303 + color = "yellow" 304 + } 305 + 306 + record := xrpc.NewHighlightRecord(req.URL, urlHash, req.Selector, color, nil) 307 + 308 + var result *xrpc.CreateRecordOutput 309 + err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 310 + var createErr error 311 + result, createErr = client.CreateRecord(r.Context(), did, xrpc.CollectionHighlight, record) 312 + return createErr 313 + }) 314 + if err != nil { 315 + http.Error(w, "Failed to create highlight: "+err.Error(), http.StatusInternalServerError) 316 + return 317 + } 318 + 319 + h.db.UpdateAPIKeyLastUsed(apiKey.ID) 320 + 321 + selectorJSON, _ := json.Marshal(req.Selector) 322 + selectorStr := string(selectorJSON) 323 + colorPtr := &color 324 + 325 + highlight := &db.Highlight{ 326 + URI: result.URI, 327 + AuthorDID: apiKey.OwnerDID, 328 + TargetSource: req.URL, 329 + TargetHash: urlHash, 330 + SelectorJSON: &selectorStr, 331 + Color: colorPtr, 332 + CreatedAt: time.Now(), 333 + IndexedAt: time.Now(), 334 + CID: &result.CID, 335 + } 336 + if err := h.db.CreateHighlight(highlight); err != nil { 337 + fmt.Printf("Warning: failed to index highlight in local DB: %v\n", err) 338 + } 339 + 340 + w.Header().Set("Content-Type", "application/json") 341 + json.NewEncoder(w).Encode(map[string]string{ 342 + "uri": result.URI, 343 + "cid": result.CID, 344 + "message": "Highlight created successfully", 345 + }) 346 + } 347 + 348 + func (h *APIKeyHandler) authenticateAPIKey(r *http.Request) (*db.APIKey, error) { 349 + auth := r.Header.Get("Authorization") 350 + if auth == "" { 351 + return nil, fmt.Errorf("missing Authorization header") 352 + } 353 + 354 + if !strings.HasPrefix(auth, "Bearer ") { 355 + return nil, fmt.Errorf("invalid Authorization format, expected 'Bearer <key>'") 356 + } 357 + 358 + rawKey := strings.TrimPrefix(auth, "Bearer ") 359 + keyHash := hashAPIKey(rawKey) 360 + 361 + apiKey, err := h.db.GetAPIKeyByHash(keyHash) 362 + if err != nil { 363 + return nil, fmt.Errorf("invalid API key") 364 + } 365 + 366 + return apiKey, nil 367 + } 368 + 369 + func (h *APIKeyHandler) getSessionByDID(did string) (*SessionData, error) { 370 + rows, err := h.db.Query(h.db.Rebind(` 371 + SELECT id, did, handle, access_token, refresh_token, COALESCE(dpop_key, '') 372 + FROM sessions 373 + WHERE did = ? AND expires_at > ? 374 + ORDER BY created_at DESC 375 + LIMIT 1 376 + `), did, time.Now()) 377 + if err != nil { 378 + return nil, err 379 + } 380 + defer rows.Close() 381 + 382 + if !rows.Next() { 383 + return nil, fmt.Errorf("no active session") 384 + } 385 + 386 + var sessionID, sessDID, handle, accessToken, refreshToken, dpopKeyStr string 387 + if err := rows.Scan(&sessionID, &sessDID, &handle, &accessToken, &refreshToken, &dpopKeyStr); err != nil { 388 + return nil, err 389 + } 390 + 391 + block, _ := pem.Decode([]byte(dpopKeyStr)) 392 + if block == nil { 393 + return nil, fmt.Errorf("invalid session DPoP key") 394 + } 395 + dpopKey, err := x509.ParseECPrivateKey(block.Bytes) 396 + if err != nil { 397 + return nil, fmt.Errorf("invalid session DPoP key: %w", err) 398 + } 399 + 400 + pds, err := resolveDIDToPDS(sessDID) 401 + if err != nil { 402 + return nil, fmt.Errorf("failed to resolve PDS: %w", err) 403 + } 404 + if pds == "" { 405 + return nil, fmt.Errorf("PDS not found for DID: %s", sessDID) 406 + } 407 + 408 + return &SessionData{ 409 + ID: sessionID, 410 + DID: sessDID, 411 + Handle: handle, 412 + AccessToken: accessToken, 413 + RefreshToken: refreshToken, 414 + DPoPKey: dpopKey, 415 + PDS: pds, 416 + }, nil 417 + } 418 + 419 + func generateAPIKey() string { 420 + b := make([]byte, 32) 421 + rand.Read(b) 422 + return "mk_" + hex.EncodeToString(b) 423 + } 424 + 425 + func generateKeyID() string { 426 + b := make([]byte, 16) 427 + rand.Read(b) 428 + return hex.EncodeToString(b) 429 + } 430 + 431 + func hashAPIKey(key string) string { 432 + h := sha256.New() 433 + h.Write([]byte(key)) 434 + return hex.EncodeToString(h.Sum(nil)) 435 + }
+117
backend/internal/api/avatar.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "io" 6 + "net/http" 7 + "net/url" 8 + "os" 9 + "sync" 10 + "time" 11 + 12 + "github.com/go-chi/chi/v5" 13 + ) 14 + 15 + type avatarCache struct { 16 + url string 17 + fetchedAt time.Time 18 + } 19 + 20 + var ( 21 + avatarCacheMu sync.RWMutex 22 + avatarCacheMap = make(map[string]avatarCache) 23 + avatarCacheTTL = 5 * time.Minute 24 + ) 25 + 26 + func (h *Handler) HandleAvatarProxy(w http.ResponseWriter, r *http.Request) { 27 + did := chi.URLParam(r, "did") 28 + if did == "" { 29 + http.Error(w, "DID required", http.StatusBadRequest) 30 + return 31 + } 32 + 33 + if decoded, err := url.QueryUnescape(did); err == nil { 34 + did = decoded 35 + } 36 + 37 + avatarURL := getAvatarURL(did) 38 + if avatarURL == "" { 39 + http.Error(w, "Avatar not found", http.StatusNotFound) 40 + return 41 + } 42 + 43 + client := &http.Client{Timeout: 10 * time.Second} 44 + resp, err := client.Get(avatarURL) 45 + if err != nil { 46 + http.Error(w, "Failed to fetch avatar", http.StatusBadGateway) 47 + return 48 + } 49 + defer resp.Body.Close() 50 + 51 + if resp.StatusCode != http.StatusOK { 52 + http.Error(w, "Avatar not available", http.StatusNotFound) 53 + return 54 + } 55 + 56 + contentType := resp.Header.Get("Content-Type") 57 + if contentType == "" { 58 + contentType = "image/jpeg" 59 + } 60 + 61 + w.Header().Set("Content-Type", contentType) 62 + w.Header().Set("Cache-Control", "public, max-age=3600") 63 + w.Header().Set("Access-Control-Allow-Origin", "*") 64 + 65 + io.Copy(w, resp.Body) 66 + } 67 + 68 + func getAvatarURL(did string) string { 69 + avatarCacheMu.RLock() 70 + if cached, ok := avatarCacheMap[did]; ok && time.Since(cached.fetchedAt) < avatarCacheTTL { 71 + avatarCacheMu.RUnlock() 72 + return cached.url 73 + } 74 + avatarCacheMu.RUnlock() 75 + 76 + q := url.Values{} 77 + q.Add("actor", did) 78 + 79 + resp, err := http.Get("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?" + q.Encode()) 80 + if err != nil { 81 + return "" 82 + } 83 + defer resp.Body.Close() 84 + 85 + if resp.StatusCode != 200 { 86 + return "" 87 + } 88 + 89 + var profile struct { 90 + Avatar string `json:"avatar"` 91 + } 92 + if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil { 93 + return "" 94 + } 95 + 96 + avatarCacheMu.Lock() 97 + avatarCacheMap[did] = avatarCache{ 98 + url: profile.Avatar, 99 + fetchedAt: time.Now(), 100 + } 101 + avatarCacheMu.Unlock() 102 + 103 + return profile.Avatar 104 + } 105 + 106 + func getProxiedAvatarURL(did, originalURL string) string { 107 + if originalURL == "" { 108 + return "" 109 + } 110 + 111 + baseURL := os.Getenv("BASE_URL") 112 + if baseURL == "" { 113 + return originalURL 114 + } 115 + 116 + return baseURL + "/api/avatar/" + url.PathEscape(did) 117 + }
+48
backend/internal/api/cache.go
··· 1 + package api 2 + 3 + import ( 4 + "sync" 5 + "time" 6 + ) 7 + 8 + type ProfileCache interface { 9 + Get(did string) (Author, bool) 10 + Set(did string, profile Author) 11 + } 12 + type InMemoryCache struct { 13 + cache sync.Map 14 + ttl time.Duration 15 + } 16 + 17 + type cachedProfile struct { 18 + Author Author 19 + ExpiresAt time.Time 20 + } 21 + 22 + func NewInMemoryCache(ttl time.Duration) *InMemoryCache { 23 + return &InMemoryCache{ 24 + ttl: ttl, 25 + } 26 + } 27 + 28 + func (c *InMemoryCache) Get(did string) (Author, bool) { 29 + val, ok := c.cache.Load(did) 30 + if !ok { 31 + return Author{}, false 32 + } 33 + 34 + entry := val.(cachedProfile) 35 + if time.Now().After(entry.ExpiresAt) { 36 + c.cache.Delete(did) 37 + return Author{}, false 38 + } 39 + 40 + return entry.Author, true 41 + } 42 + 43 + func (c *InMemoryCache) Set(did string, profile Author) { 44 + c.cache.Store(did, cachedProfile{ 45 + Author: profile, 46 + ExpiresAt: time.Now().Add(c.ttl), 47 + }) 48 + }
+16 -1
backend/internal/api/handler.go
··· 19 19 db *db.DB 20 20 annotationService *AnnotationService 21 21 refresher *TokenRefresher 22 + apiKeys *APIKeyHandler 22 23 } 23 24 24 25 func NewHandler(database *db.DB, annotationService *AnnotationService, refresher *TokenRefresher) *Handler { 25 - return &Handler{db: database, annotationService: annotationService, refresher: refresher} 26 + return &Handler{ 27 + db: database, 28 + annotationService: annotationService, 29 + refresher: refresher, 30 + apiKeys: NewAPIKeyHandler(database, refresher), 31 + } 26 32 } 27 33 28 34 func (h *Handler) RegisterRoutes(r chi.Router) { ··· 64 70 r.Get("/notifications", h.GetNotifications) 65 71 r.Get("/notifications/count", h.GetUnreadNotificationCount) 66 72 r.Post("/notifications/read", h.MarkNotificationsRead) 73 + r.Get("/avatar/{did}", h.HandleAvatarProxy) 74 + 75 + r.Post("/keys", h.apiKeys.CreateKey) 76 + r.Get("/keys", h.apiKeys.ListKeys) 77 + r.Delete("/keys/{id}", h.apiKeys.DeleteKey) 78 + 79 + r.Post("/quick/bookmark", h.apiKeys.QuickBookmark) 80 + r.Post("/quick/annotation", h.apiKeys.QuickAnnotation) 81 + r.Post("/quick/highlight", h.apiKeys.QuickHighlight) 67 82 }) 68 83 } 69 84
+169 -82
backend/internal/api/hydration.go
··· 13 13 "margin.at/internal/db" 14 14 ) 15 15 16 + var ( 17 + Cache ProfileCache = NewInMemoryCache(5 * time.Minute) 18 + ) 19 + 16 20 type Author struct { 17 21 DID string `json:"did"` 18 22 Handle string `json:"handle"` ··· 148 152 149 153 profiles := fetchProfilesForDIDs(collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID })) 150 154 155 + var likeCounts map[string]int 156 + var replyCounts map[string]int 157 + var viewerLikes map[string]bool 158 + 159 + if database != nil { 160 + uris := make([]string, len(annotations)) 161 + for i, a := range annotations { 162 + uris[i] = a.URI 163 + } 164 + 165 + likeCounts, _ = database.GetLikeCounts(uris) 166 + replyCounts, _ = database.GetReplyCounts(uris) 167 + if viewerDID != "" { 168 + viewerLikes, _ = database.GetViewerLikes(viewerDID, uris) 169 + } 170 + } 171 + 151 172 result := make([]APIAnnotation, len(annotations)) 152 173 for i, a := range annotations { 153 174 var body *APIBody ··· 208 229 } 209 230 210 231 if database != nil { 211 - result[i].LikeCount, _ = database.GetLikeCount(a.URI) 212 - result[i].ReplyCount, _ = database.GetReplyCount(a.URI) 213 - if viewerDID != "" { 214 - if _, err := database.GetLikeByUserAndSubject(viewerDID, a.URI); err == nil { 215 - result[i].ViewerHasLiked = true 216 - } 232 + result[i].LikeCount = likeCounts[a.URI] 233 + result[i].ReplyCount = replyCounts[a.URI] 234 + if viewerLikes != nil && viewerLikes[a.URI] { 235 + result[i].ViewerHasLiked = true 217 236 } 218 237 } 219 238 } ··· 228 247 229 248 profiles := fetchProfilesForDIDs(collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID })) 230 249 250 + var likeCounts map[string]int 251 + var replyCounts map[string]int 252 + var viewerLikes map[string]bool 253 + 254 + if database != nil { 255 + uris := make([]string, len(highlights)) 256 + for i, h := range highlights { 257 + uris[i] = h.URI 258 + } 259 + 260 + likeCounts, _ = database.GetLikeCounts(uris) 261 + replyCounts, _ = database.GetReplyCounts(uris) 262 + if viewerDID != "" { 263 + viewerLikes, _ = database.GetViewerLikes(viewerDID, uris) 264 + } 265 + } 266 + 231 267 result := make([]APIHighlight, len(highlights)) 232 268 for i, h := range highlights { 233 269 var selector *APISelector ··· 272 308 } 273 309 274 310 if database != nil { 275 - result[i].LikeCount, _ = database.GetLikeCount(h.URI) 276 - result[i].ReplyCount, _ = database.GetReplyCount(h.URI) 277 - if viewerDID != "" { 278 - if _, err := database.GetLikeByUserAndSubject(viewerDID, h.URI); err == nil { 279 - result[i].ViewerHasLiked = true 280 - } 311 + result[i].LikeCount = likeCounts[h.URI] 312 + result[i].ReplyCount = replyCounts[h.URI] 313 + if viewerLikes != nil && viewerLikes[h.URI] { 314 + result[i].ViewerHasLiked = true 281 315 } 282 316 } 283 317 } ··· 292 326 293 327 profiles := fetchProfilesForDIDs(collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID })) 294 328 329 + var likeCounts map[string]int 330 + var replyCounts map[string]int 331 + var viewerLikes map[string]bool 332 + 333 + if database != nil { 334 + uris := make([]string, len(bookmarks)) 335 + for i, b := range bookmarks { 336 + uris[i] = b.URI 337 + } 338 + 339 + likeCounts, _ = database.GetLikeCounts(uris) 340 + replyCounts, _ = database.GetReplyCounts(uris) 341 + if viewerDID != "" { 342 + viewerLikes, _ = database.GetViewerLikes(viewerDID, uris) 343 + } 344 + } 345 + 295 346 result := make([]APIBookmark, len(bookmarks)) 296 347 for i, b := range bookmarks { 297 348 var tags []string ··· 326 377 CID: cid, 327 378 } 328 379 if database != nil { 329 - result[i].LikeCount, _ = database.GetLikeCount(b.URI) 330 - result[i].ReplyCount, _ = database.GetReplyCount(b.URI) 331 - if viewerDID != "" { 332 - if _, err := database.GetLikeByUserAndSubject(viewerDID, b.URI); err == nil { 333 - result[i].ViewerHasLiked = true 334 - } 380 + result[i].LikeCount = likeCounts[b.URI] 381 + result[i].ReplyCount = replyCounts[b.URI] 382 + if viewerLikes != nil && viewerLikes[b.URI] { 383 + result[i].ViewerHasLiked = true 335 384 } 336 385 } 337 386 } ··· 388 437 389 438 func fetchProfilesForDIDs(dids []string) map[string]Author { 390 439 profiles := make(map[string]Author) 440 + missingDIDs := make([]string, 0) 391 441 392 442 for _, did := range dids { 393 - profiles[did] = Author{ 394 - DID: did, 395 - Handle: "unknown", 443 + if author, ok := Cache.Get(did); ok { 444 + profiles[did] = author 445 + } else { 446 + missingDIDs = append(missingDIDs, did) 396 447 } 397 448 } 398 449 399 - if len(dids) == 0 { 450 + if len(missingDIDs) == 0 { 400 451 return profiles 401 452 } 402 453 ··· 404 455 var wg sync.WaitGroup 405 456 var mu sync.Mutex 406 457 407 - for i := 0; i < len(dids); i += batchSize { 458 + for i := 0; i < len(missingDIDs); i += batchSize { 408 459 end := i + batchSize 409 - if end > len(dids) { 410 - end = len(dids) 460 + if end > len(missingDIDs) { 461 + end = len(missingDIDs) 411 462 } 412 - batch := dids[i:end] 463 + batch := missingDIDs[i:end] 413 464 414 465 wg.Add(1) 415 466 go func(actors []string) { ··· 417 468 fetched, err := fetchProfiles(actors) 418 469 if err == nil { 419 470 mu.Lock() 471 + defer mu.Unlock() 420 472 for k, v := range fetched { 421 473 profiles[k] = v 474 + Cache.Set(k, v) 422 475 } 423 - mu.Unlock() 424 476 } 425 477 }(batch) 426 478 } ··· 470 522 DID: p.DID, 471 523 Handle: p.Handle, 472 524 DisplayName: p.DisplayName, 473 - Avatar: p.Avatar, 525 + Avatar: getProxiedAvatarURL(p.DID, p.Avatar), 474 526 } 475 527 } 476 528 ··· 484 536 485 537 profiles := fetchProfilesForDIDs(collectDIDs(items, func(i db.CollectionItem) string { return i.AuthorDID })) 486 538 539 + var collectionURIs []string 540 + var annotationURIs []string 541 + var highlightURIs []string 542 + var bookmarkURIs []string 543 + 544 + for _, item := range items { 545 + collectionURIs = append(collectionURIs, item.CollectionURI) 546 + if strings.Contains(item.AnnotationURI, "at.margin.annotation") { 547 + annotationURIs = append(annotationURIs, item.AnnotationURI) 548 + } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") { 549 + highlightURIs = append(highlightURIs, item.AnnotationURI) 550 + } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") { 551 + bookmarkURIs = append(bookmarkURIs, item.AnnotationURI) 552 + } 553 + } 554 + 555 + collectionsMap := make(map[string]APICollection) 556 + if len(collectionURIs) > 0 { 557 + colls, err := database.GetCollectionsByURIs(collectionURIs) 558 + if err == nil { 559 + collProfiles := fetchProfilesForDIDs(collectDIDs(colls, func(c db.Collection) string { return c.AuthorDID })) 560 + for _, coll := range colls { 561 + icon := "" 562 + if coll.Icon != nil { 563 + icon = *coll.Icon 564 + } 565 + desc := "" 566 + if coll.Description != nil { 567 + desc = *coll.Description 568 + } 569 + collectionsMap[coll.URI] = APICollection{ 570 + URI: coll.URI, 571 + Name: coll.Name, 572 + Description: desc, 573 + Icon: icon, 574 + Creator: collProfiles[coll.AuthorDID], 575 + CreatedAt: coll.CreatedAt, 576 + IndexedAt: coll.IndexedAt, 577 + } 578 + } 579 + } 580 + } 581 + 582 + annotationsMap := make(map[string]APIAnnotation) 583 + if len(annotationURIs) > 0 { 584 + rawAnnos, err := database.GetAnnotationsByURIs(annotationURIs) 585 + if err == nil { 586 + hydrated, _ := hydrateAnnotations(database, rawAnnos, viewerDID) 587 + for _, a := range hydrated { 588 + annotationsMap[a.ID] = a 589 + } 590 + } 591 + } 592 + 593 + highlightsMap := make(map[string]APIHighlight) 594 + if len(highlightURIs) > 0 { 595 + rawHighlights, err := database.GetHighlightsByURIs(highlightURIs) 596 + if err == nil { 597 + hydrated, _ := hydrateHighlights(database, rawHighlights, viewerDID) 598 + for _, h := range hydrated { 599 + highlightsMap[h.ID] = h 600 + } 601 + } 602 + } 603 + 604 + bookmarksMap := make(map[string]APIBookmark) 605 + if len(bookmarkURIs) > 0 { 606 + rawBookmarks, err := database.GetBookmarksByURIs(bookmarkURIs) 607 + if err == nil { 608 + hydrated, _ := hydrateBookmarks(database, rawBookmarks, viewerDID) 609 + for _, b := range hydrated { 610 + bookmarksMap[b.ID] = b 611 + } 612 + } 613 + } 614 + 487 615 result := make([]APICollectionItem, len(items)) 488 616 for i, item := range items { 489 617 apiItem := APICollectionItem{ ··· 495 623 Position: item.Position, 496 624 } 497 625 498 - if coll, err := database.GetCollectionByURI(item.CollectionURI); err == nil { 499 - icon := "" 500 - if coll.Icon != nil { 501 - icon = *coll.Icon 502 - } 503 - desc := "" 504 - if coll.Description != nil { 505 - desc = *coll.Description 506 - } 507 - apiItem.Collection = &APICollection{ 508 - URI: coll.URI, 509 - Name: coll.Name, 510 - Description: desc, 511 - Icon: icon, 512 - Creator: profiles[coll.AuthorDID], 513 - CreatedAt: coll.CreatedAt, 514 - IndexedAt: coll.IndexedAt, 515 - } 626 + if coll, ok := collectionsMap[item.CollectionURI]; ok { 627 + apiItem.Collection = &coll 516 628 } 517 629 518 - if strings.Contains(item.AnnotationURI, "at.margin.annotation") { 519 - if a, err := database.GetAnnotationByURI(item.AnnotationURI); err == nil { 520 - hydrated, _ := hydrateAnnotations(database, []db.Annotation{*a}, viewerDID) 521 - if len(hydrated) > 0 { 522 - apiItem.Annotation = &hydrated[0] 523 - } 524 - } 525 - } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") { 526 - if h, err := database.GetHighlightByURI(item.AnnotationURI); err == nil { 527 - hydrated, _ := hydrateHighlights(database, []db.Highlight{*h}, viewerDID) 528 - if len(hydrated) > 0 { 529 - apiItem.Highlight = &hydrated[0] 530 - } 531 - } 532 - } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") { 533 - if b, err := database.GetBookmarkByURI(item.AnnotationURI); err == nil { 534 - hydrated, _ := hydrateBookmarks(database, []db.Bookmark{*b}, viewerDID) 535 - if len(hydrated) > 0 { 536 - apiItem.Bookmark = &hydrated[0] 537 - } else { 538 - log.Printf("Failed to hydrate bookmark %s: empty hydration result\n", item.AnnotationURI) 539 - } 540 - } else { 541 - } 542 - } else { 543 - log.Printf("Unknown item type for URI: %s\n", item.AnnotationURI) 630 + if val, ok := annotationsMap[item.AnnotationURI]; ok { 631 + apiItem.Annotation = &val 632 + } else if val, ok := highlightsMap[item.AnnotationURI]; ok { 633 + apiItem.Highlight = &val 634 + } else if val, ok := bookmarksMap[item.AnnotationURI]; ok { 635 + apiItem.Bookmark = &val 544 636 } 545 637 546 638 result[i] = apiItem ··· 577 669 578 670 replyMap := make(map[string]APIReply) 579 671 if len(replyURIs) > 0 { 580 - var replies []db.Reply 581 - for _, uri := range replyURIs { 582 - r, err := database.GetReplyByURI(uri) 583 - if err == nil { 584 - replies = append(replies, *r) 672 + replies, err := database.GetRepliesByURIs(replyURIs) 673 + if err == nil { 674 + hydratedReplies, _ := hydrateReplies(replies) 675 + for _, r := range hydratedReplies { 676 + replyMap[r.ID] = r 585 677 } 586 - } 587 - 588 - hydratedReplies, _ := hydrateReplies(replies) 589 - for _, r := range hydratedReplies { 590 - replyMap[r.ID] = r 591 678 } 592 679 } 593 680
+10 -4
backend/internal/api/token_refresh.go
··· 52 52 } 53 53 54 54 type SessionData struct { 55 + ID string 55 56 DID string 56 57 Handle string 57 58 AccessToken string ··· 94 95 } 95 96 96 97 return &SessionData{ 98 + ID: sessionID, 97 99 DID: did, 98 100 Handle: handle, 99 101 AccessToken: accessToken, ··· 104 106 } 105 107 106 108 func (tr *TokenRefresher) RefreshSessionToken(r *http.Request, session *SessionData) (*SessionData, error) { 107 - cookie, err := r.Cookie("margin_session") 108 - if err != nil { 109 - return nil, fmt.Errorf("not authenticated") 109 + if session.ID == "" { 110 + return nil, fmt.Errorf("invalid session ID") 110 111 } 111 112 112 113 oauthClient := tr.getOAuthClient(r) ··· 138 139 139 140 expiresAt := time.Now().Add(7 * 24 * time.Hour) 140 141 if err := tr.db.SaveSession( 141 - cookie.Value, 142 + session.ID, 142 143 session.DID, 143 144 session.Handle, 144 145 tokenResp.AccessToken, ··· 152 153 log.Printf("Successfully refreshed token for user %s", session.Handle) 153 154 154 155 return &SessionData{ 156 + ID: session.ID, 155 157 DID: session.DID, 156 158 Handle: session.Handle, 157 159 AccessToken: tokenResp.AccessToken, ··· 196 198 client = xrpc.NewClient(newSession.PDS, newSession.AccessToken, newSession.DPoPKey) 197 199 return fn(client, newSession.DID) 198 200 } 201 + 202 + func (tr *TokenRefresher) CreateClientFromSession(session *SessionData) *xrpc.Client { 203 + return xrpc.NewClient(session.PDS, session.AccessToken, session.DPoPKey) 204 + }
+26 -1
backend/internal/db/db.go
··· 120 120 ReadAt *time.Time `json:"readAt,omitempty"` 121 121 } 122 122 123 + type APIKey struct { 124 + ID string `json:"id"` 125 + OwnerDID string `json:"ownerDid"` 126 + Name string `json:"name"` 127 + KeyHash string `json:"-"` 128 + CreatedAt time.Time `json:"createdAt"` 129 + LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` 130 + } 131 + 123 132 func New(dsn string) (*DB, error) { 124 133 driver := "sqlite3" 125 134 if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { ··· 232 241 )`) 233 242 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`) 234 243 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`) 244 + db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_subject ON likes(author_did, subject_uri)`) 235 245 236 246 db.Exec(`CREATE TABLE IF NOT EXISTS collections ( 237 247 uri TEXT PRIMARY KEY, ··· 296 306 db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_did)`) 297 307 db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC)`) 298 308 309 + db.Exec(`CREATE TABLE IF NOT EXISTS api_keys ( 310 + id TEXT PRIMARY KEY, 311 + owner_did TEXT NOT NULL, 312 + name TEXT NOT NULL, 313 + key_hash TEXT NOT NULL, 314 + created_at ` + dateType + ` NOT NULL, 315 + last_used_at ` + dateType + ` 316 + )`) 317 + db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner_did)`) 318 + db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`) 319 + 299 320 db.runMigrations() 300 321 301 322 db.Exec(`CREATE TABLE IF NOT EXISTS cursors ( 302 323 id TEXT PRIMARY KEY, 303 - last_cursor INTEGER NOT NULL, 324 + last_cursor BIGINT NOT NULL, 304 325 updated_at ` + dateType + ` NOT NULL 305 326 )`) 306 327 ··· 353 374 db.Exec(`UPDATE annotations SET body_value = text WHERE body_value IS NULL AND text IS NOT NULL`) 354 375 db.Exec(`UPDATE annotations SET target_title = title WHERE target_title IS NULL AND title IS NOT NULL`) 355 376 db.Exec(`UPDATE annotations SET motivation = 'commenting' WHERE motivation IS NULL`) 377 + 378 + if db.driver == "postgres" { 379 + db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`) 380 + } 356 381 } 357 382 358 383 func (db *DB) Close() error {
+14 -825
backend/internal/db/queries.go
··· 10 10 "time" 11 11 ) 12 12 13 - func (db *DB) CreateAnnotation(a *Annotation) error { 14 - _, err := db.Exec(db.Rebind(` 15 - INSERT INTO annotations (uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid) 16 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 17 - ON CONFLICT(uri) DO UPDATE SET 18 - motivation = excluded.motivation, 19 - body_value = excluded.body_value, 20 - body_format = excluded.body_format, 21 - body_uri = excluded.body_uri, 22 - target_title = excluded.target_title, 23 - selector_json = excluded.selector_json, 24 - tags_json = excluded.tags_json, 25 - indexed_at = excluded.indexed_at, 26 - cid = excluded.cid 27 - `), a.URI, a.AuthorDID, a.Motivation, a.BodyValue, a.BodyFormat, a.BodyURI, a.TargetSource, a.TargetHash, a.TargetTitle, a.SelectorJSON, a.TagsJSON, a.CreatedAt, a.IndexedAt, a.CID) 28 - return err 29 - } 30 - 31 - func (db *DB) GetAnnotationByURI(uri string) (*Annotation, error) { 32 - var a Annotation 33 - err := db.QueryRow(db.Rebind(` 34 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 35 - FROM annotations 36 - WHERE uri = ? 37 - `), uri).Scan(&a.URI, &a.AuthorDID, &a.Motivation, &a.BodyValue, &a.BodyFormat, &a.BodyURI, &a.TargetSource, &a.TargetHash, &a.TargetTitle, &a.SelectorJSON, &a.TagsJSON, &a.CreatedAt, &a.IndexedAt, &a.CID) 38 - if err != nil { 39 - return nil, err 40 - } 41 - return &a, nil 42 - } 43 - 44 - func (db *DB) GetAnnotationsByTargetHash(targetHash string, limit, offset int) ([]Annotation, error) { 45 - rows, err := db.Query(db.Rebind(` 46 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 47 - FROM annotations 48 - WHERE target_hash = ? 49 - ORDER BY created_at DESC 50 - LIMIT ? OFFSET ? 51 - `), targetHash, limit, offset) 52 - if err != nil { 53 - return nil, err 54 - } 55 - defer rows.Close() 56 - 57 - return scanAnnotations(rows) 58 - } 59 - 60 - func (db *DB) GetAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) { 61 - rows, err := db.Query(db.Rebind(` 62 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 63 - FROM annotations 64 - WHERE author_did = ? 65 - ORDER BY created_at DESC 66 - LIMIT ? OFFSET ? 67 - `), authorDID, limit, offset) 68 - if err != nil { 69 - return nil, err 70 - } 71 - defer rows.Close() 72 - 73 - return scanAnnotations(rows) 74 - } 75 - 76 - func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) { 77 - rows, err := db.Query(db.Rebind(` 78 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 79 - FROM annotations 80 - WHERE motivation = ? 81 - ORDER BY created_at DESC 82 - LIMIT ? OFFSET ? 83 - `), motivation, limit, offset) 84 - if err != nil { 85 - return nil, err 86 - } 87 - defer rows.Close() 88 - 89 - return scanAnnotations(rows) 90 - } 91 - 92 - func (db *DB) GetRecentAnnotations(limit, offset int) ([]Annotation, error) { 93 - rows, err := db.Query(db.Rebind(` 94 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 95 - FROM annotations 96 - ORDER BY created_at DESC 97 - LIMIT ? OFFSET ? 98 - `), limit, offset) 99 - if err != nil { 100 - return nil, err 101 - } 102 - defer rows.Close() 103 - 104 - return scanAnnotations(rows) 105 - } 106 - 107 - func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) { 108 - pattern := "%\"" + tag + "\"%" 109 - rows, err := db.Query(db.Rebind(` 110 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 111 - FROM annotations 112 - WHERE tags_json LIKE ? 113 - ORDER BY created_at DESC 114 - LIMIT ? OFFSET ? 115 - `), pattern, limit, offset) 116 - if err != nil { 117 - return nil, err 118 - } 119 - defer rows.Close() 120 - 121 - return scanAnnotations(rows) 122 - } 123 - 124 - func (db *DB) DeleteAnnotation(uri string) error { 125 - _, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri) 126 - return err 127 - } 128 - 129 - func (db *DB) UpdateAnnotation(uri, bodyValue, tagsJSON, cid string) error { 130 - _, err := db.Exec(db.Rebind(` 131 - UPDATE annotations 132 - SET body_value = ?, tags_json = ?, cid = ?, indexed_at = ? 133 - WHERE uri = ? 134 - `), bodyValue, tagsJSON, cid, time.Now(), uri) 135 - return err 136 - } 137 - 138 - func (db *DB) UpdateHighlight(uri, color, tagsJSON, cid string) error { 139 - _, err := db.Exec(db.Rebind(` 140 - UPDATE highlights 141 - SET color = ?, tags_json = ?, cid = ?, indexed_at = ? 142 - WHERE uri = ? 143 - `), color, tagsJSON, cid, time.Now(), uri) 144 - return err 145 - } 146 - 147 - func (db *DB) UpdateBookmark(uri, title, description, tagsJSON, cid string) error { 148 - _, err := db.Exec(db.Rebind(` 149 - UPDATE bookmarks 150 - SET title = ?, description = ?, tags_json = ?, cid = ?, indexed_at = ? 151 - WHERE uri = ? 152 - `), title, description, tagsJSON, cid, time.Now(), uri) 153 - return err 154 - } 155 - 156 13 type EditHistory struct { 157 14 ID int `json:"id"` 158 15 URI string `json:"uri"` ··· 162 19 EditedAt time.Time `json:"editedAt"` 163 20 } 164 21 165 - func (db *DB) SaveEditHistory(uri, recordType, previousContent string, previousCID *string) error { 166 - _, err := db.Exec(db.Rebind(` 167 - INSERT INTO edit_history (uri, record_type, previous_content, previous_cid, edited_at) 168 - VALUES (?, ?, ?, ?, ?) 169 - `), uri, recordType, previousContent, previousCID, time.Now()) 170 - return err 171 - } 172 - 173 - func (db *DB) GetEditHistory(uri string) ([]EditHistory, error) { 174 - rows, err := db.Query(db.Rebind(` 175 - SELECT id, uri, record_type, previous_content, previous_cid, edited_at 176 - FROM edit_history 177 - WHERE uri = ? 178 - ORDER BY edited_at DESC 179 - `), uri) 180 - if err != nil { 181 - return nil, err 182 - } 183 - defer rows.Close() 184 - 185 - var history []EditHistory 186 - for rows.Next() { 187 - var h EditHistory 188 - if err := rows.Scan(&h.ID, &h.URI, &h.RecordType, &h.PreviousContent, &h.PreviousCID, &h.EditedAt); err != nil { 189 - return nil, err 190 - } 191 - history = append(history, h) 192 - } 193 - return history, nil 194 - } 195 - 196 22 func scanAnnotations(rows interface { 197 23 Next() bool 198 24 Scan(...interface{}) error ··· 208 34 return annotations, nil 209 35 } 210 36 211 - func (db *DB) CreateHighlight(h *Highlight) error { 212 - _, err := db.Exec(db.Rebind(` 213 - INSERT INTO highlights (uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid) 214 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 215 - ON CONFLICT(uri) DO UPDATE SET 216 - target_title = excluded.target_title, 217 - selector_json = excluded.selector_json, 218 - color = excluded.color, 219 - tags_json = excluded.tags_json, 220 - indexed_at = excluded.indexed_at, 221 - cid = excluded.cid 222 - `), h.URI, h.AuthorDID, h.TargetSource, h.TargetHash, h.TargetTitle, h.SelectorJSON, h.Color, h.TagsJSON, h.CreatedAt, h.IndexedAt, h.CID) 223 - return err 224 - } 225 - 226 - func (db *DB) GetHighlightByURI(uri string) (*Highlight, error) { 227 - var h Highlight 228 - err := db.QueryRow(db.Rebind(` 229 - SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 230 - FROM highlights 231 - WHERE uri = ? 232 - `), uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID) 233 - if err != nil { 234 - return nil, err 235 - } 236 - return &h, nil 237 - } 238 - 239 - func (db *DB) GetRecentHighlights(limit, offset int) ([]Highlight, error) { 240 - rows, err := db.Query(db.Rebind(` 241 - SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 242 - FROM highlights 243 - ORDER BY created_at DESC 244 - LIMIT ? OFFSET ? 245 - `), limit, offset) 246 - if err != nil { 247 - return nil, err 248 - } 249 - defer rows.Close() 250 - 251 - var highlights []Highlight 252 - for rows.Next() { 253 - var h Highlight 254 - if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 255 - return nil, err 256 - } 257 - highlights = append(highlights, h) 258 - } 259 - return highlights, nil 260 - } 261 - 262 - func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) { 263 - pattern := "%\"" + tag + "\"%" 264 - rows, err := db.Query(db.Rebind(` 265 - SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 266 - FROM highlights 267 - WHERE tags_json LIKE ? 268 - ORDER BY created_at DESC 269 - LIMIT ? OFFSET ? 270 - `), pattern, limit, offset) 271 - if err != nil { 272 - return nil, err 273 - } 274 - defer rows.Close() 275 - 276 - var highlights []Highlight 277 - for rows.Next() { 278 - var h Highlight 279 - if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 280 - return nil, err 281 - } 282 - highlights = append(highlights, h) 283 - } 284 - return highlights, nil 285 - } 286 - 287 - func (db *DB) GetRecentBookmarks(limit, offset int) ([]Bookmark, error) { 288 - rows, err := db.Query(db.Rebind(` 289 - SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 290 - FROM bookmarks 291 - ORDER BY created_at DESC 292 - LIMIT ? OFFSET ? 293 - `), limit, offset) 294 - if err != nil { 295 - return nil, err 296 - } 297 - defer rows.Close() 298 - 299 - var bookmarks []Bookmark 300 - for rows.Next() { 301 - var b Bookmark 302 - if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 303 - return nil, err 304 - } 305 - bookmarks = append(bookmarks, b) 306 - } 307 - return bookmarks, nil 308 - } 309 - 310 - func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) { 311 - pattern := "%\"" + tag + "\"%" 312 - rows, err := db.Query(db.Rebind(` 313 - SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 314 - FROM bookmarks 315 - WHERE tags_json LIKE ? 316 - ORDER BY created_at DESC 317 - LIMIT ? OFFSET ? 318 - `), pattern, limit, offset) 319 - if err != nil { 320 - return nil, err 321 - } 322 - defer rows.Close() 323 - 324 - var bookmarks []Bookmark 325 - for rows.Next() { 326 - var b Bookmark 327 - if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 328 - return nil, err 329 - } 330 - bookmarks = append(bookmarks, b) 331 - } 332 - return bookmarks, nil 333 - } 334 - 335 - func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) { 336 - pattern := "%\"" + tag + "\"%" 337 - rows, err := db.Query(db.Rebind(` 338 - SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 339 - FROM annotations 340 - WHERE author_did = ? AND tags_json LIKE ? 341 - ORDER BY created_at DESC 342 - LIMIT ? OFFSET ? 343 - `), authorDID, pattern, limit, offset) 344 - if err != nil { 345 - return nil, err 346 - } 347 - defer rows.Close() 348 - 349 - return scanAnnotations(rows) 350 - } 351 - 352 - func (db *DB) GetHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) { 353 - pattern := "%\"" + tag + "\"%" 354 - rows, err := db.Query(db.Rebind(` 355 - SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 356 - FROM highlights 357 - WHERE author_did = ? AND tags_json LIKE ? 358 - ORDER BY created_at DESC 359 - LIMIT ? OFFSET ? 360 - `), authorDID, pattern, limit, offset) 361 - if err != nil { 362 - return nil, err 363 - } 364 - defer rows.Close() 365 - 366 - var highlights []Highlight 367 - for rows.Next() { 368 - var h Highlight 369 - if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 370 - return nil, err 371 - } 372 - highlights = append(highlights, h) 373 - } 374 - return highlights, nil 375 - } 376 - 377 - func (db *DB) GetBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) { 378 - pattern := "%\"" + tag + "\"%" 379 - rows, err := db.Query(db.Rebind(` 380 - SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 381 - FROM bookmarks 382 - WHERE author_did = ? AND tags_json LIKE ? 383 - ORDER BY created_at DESC 384 - LIMIT ? OFFSET ? 385 - `), authorDID, pattern, limit, offset) 386 - if err != nil { 387 - return nil, err 388 - } 389 - defer rows.Close() 390 - 391 - var bookmarks []Bookmark 392 - for rows.Next() { 393 - var b Bookmark 394 - if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 395 - return nil, err 396 - } 397 - bookmarks = append(bookmarks, b) 398 - } 399 - return bookmarks, nil 400 - } 401 - 402 - func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) { 403 - rows, err := db.Query(db.Rebind(` 404 - SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 405 - FROM highlights 406 - WHERE target_hash = ? 407 - ORDER BY created_at DESC 408 - LIMIT ? OFFSET ? 409 - `), targetHash, limit, offset) 410 - if err != nil { 411 - return nil, err 412 - } 413 - defer rows.Close() 414 - 415 - var highlights []Highlight 416 - for rows.Next() { 417 - var h Highlight 418 - if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 419 - return nil, err 420 - } 421 - highlights = append(highlights, h) 422 - } 423 - return highlights, nil 424 - } 425 - 426 - func (db *DB) GetHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) { 427 - rows, err := db.Query(db.Rebind(` 428 - SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 429 - FROM highlights 430 - WHERE author_did = ? 431 - ORDER BY created_at DESC 432 - LIMIT ? OFFSET ? 433 - `), authorDID, limit, offset) 434 - if err != nil { 435 - return nil, err 436 - } 437 - defer rows.Close() 438 - 439 - var highlights []Highlight 440 - for rows.Next() { 441 - var h Highlight 442 - if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 443 - return nil, err 444 - } 445 - highlights = append(highlights, h) 446 - } 447 - return highlights, nil 448 - } 449 - 450 - func (db *DB) DeleteHighlight(uri string) error { 451 - _, err := db.Exec(db.Rebind(`DELETE FROM highlights WHERE uri = ?`), uri) 452 - return err 453 - } 454 - 455 - func (db *DB) CreateBookmark(b *Bookmark) error { 456 - _, err := db.Exec(db.Rebind(` 457 - INSERT INTO bookmarks (uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid) 458 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 459 - ON CONFLICT(uri) DO UPDATE SET 460 - title = excluded.title, 461 - description = excluded.description, 462 - tags_json = excluded.tags_json, 463 - indexed_at = excluded.indexed_at, 464 - cid = excluded.cid 465 - `), b.URI, b.AuthorDID, b.Source, b.SourceHash, b.Title, b.Description, b.TagsJSON, b.CreatedAt, b.IndexedAt, b.CID) 466 - return err 467 - } 468 - 469 - func (db *DB) GetBookmarkByURI(uri string) (*Bookmark, error) { 470 - var b Bookmark 471 - err := db.QueryRow(db.Rebind(` 472 - SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 473 - FROM bookmarks 474 - WHERE uri = ? 475 - `), uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID) 476 - if err != nil { 477 - return nil, err 478 - } 479 - return &b, nil 480 - } 481 - 482 - func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) { 483 - rows, err := db.Query(db.Rebind(` 484 - SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 485 - FROM bookmarks 486 - WHERE author_did = ? 487 - ORDER BY created_at DESC 488 - LIMIT ? OFFSET ? 489 - `), authorDID, limit, offset) 490 - if err != nil { 491 - return nil, err 492 - } 493 - defer rows.Close() 494 - 495 - var bookmarks []Bookmark 496 - for rows.Next() { 497 - var b Bookmark 498 - if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 499 - return nil, err 500 - } 501 - bookmarks = append(bookmarks, b) 502 - } 503 - return bookmarks, nil 504 - } 505 - 506 - func (db *DB) DeleteBookmark(uri string) error { 507 - _, err := db.Exec(db.Rebind(`DELETE FROM bookmarks WHERE uri = ?`), uri) 508 - return err 509 - } 510 - 511 - func (db *DB) CreateReply(r *Reply) error { 512 - _, err := db.Exec(db.Rebind(` 513 - INSERT INTO replies (uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid) 514 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 515 - ON CONFLICT(uri) DO UPDATE SET 516 - text = excluded.text, 517 - format = excluded.format, 518 - indexed_at = excluded.indexed_at, 519 - cid = excluded.cid 520 - `), r.URI, r.AuthorDID, r.ParentURI, r.RootURI, r.Text, r.Format, r.CreatedAt, r.IndexedAt, r.CID) 521 - return err 522 - } 523 - 524 - func (db *DB) GetRepliesByRoot(rootURI string) ([]Reply, error) { 525 - rows, err := db.Query(db.Rebind(` 526 - SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 527 - FROM replies 528 - WHERE root_uri = ? 529 - ORDER BY created_at ASC 530 - `), rootURI) 531 - if err != nil { 532 - return nil, err 533 - } 534 - defer rows.Close() 535 - 536 - var replies []Reply 537 - for rows.Next() { 538 - var r Reply 539 - if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 540 - return nil, err 541 - } 542 - replies = append(replies, r) 543 - } 544 - return replies, nil 545 - } 546 - 547 - func (db *DB) GetReplyByURI(uri string) (*Reply, error) { 548 - var r Reply 549 - err := db.QueryRow(db.Rebind(` 550 - SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 551 - FROM replies 552 - WHERE uri = ? 553 - `), uri).Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID) 554 - if err != nil { 555 - return nil, err 556 - } 557 - return &r, nil 558 - } 559 - 560 - func (db *DB) DeleteReply(uri string) error { 561 - _, err := db.Exec(db.Rebind(`DELETE FROM replies WHERE uri = ?`), uri) 562 - return err 563 - } 564 - 565 - func (db *DB) GetRepliesByAuthor(authorDID string) ([]Reply, error) { 566 - rows, err := db.Query(db.Rebind(` 567 - SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 568 - FROM replies 569 - WHERE author_did = ? 570 - ORDER BY created_at DESC 571 - `), authorDID) 572 - if err != nil { 573 - return nil, err 574 - } 575 - defer rows.Close() 576 - 577 - var replies []Reply 578 - for rows.Next() { 579 - var r Reply 580 - if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 581 - return nil, err 582 - } 583 - replies = append(replies, r) 584 - } 585 - return replies, nil 586 - } 587 - 588 37 func (db *DB) AnnotationExists(uri string) bool { 589 38 var count int 590 39 db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM annotations WHERE uri = ?`), uri).Scan(&count) 591 40 return count > 0 592 41 } 593 42 594 - func (db *DB) GetOrphanedRepliesByAuthor(authorDID string) ([]Reply, error) { 595 - rows, err := db.Query(db.Rebind(` 596 - SELECT r.uri, r.author_did, r.parent_uri, r.root_uri, r.text, r.format, r.created_at, r.indexed_at, r.cid 597 - FROM replies r 598 - LEFT JOIN annotations a ON r.root_uri = a.uri 599 - WHERE r.author_did = ? AND a.uri IS NULL 600 - `), authorDID) 601 - if err != nil { 602 - return nil, err 603 - } 604 - defer rows.Close() 605 - 606 - var replies []Reply 607 - for rows.Next() { 608 - var r Reply 609 - if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 610 - return nil, err 611 - } 612 - replies = append(replies, r) 613 - } 614 - return replies, nil 615 - } 616 - 617 - func (db *DB) CreateLike(l *Like) error { 618 - _, err := db.Exec(db.Rebind(` 619 - INSERT INTO likes (uri, author_did, subject_uri, created_at, indexed_at) 620 - VALUES (?, ?, ?, ?, ?) 621 - ON CONFLICT(uri) DO NOTHING 622 - `), l.URI, l.AuthorDID, l.SubjectURI, l.CreatedAt, l.IndexedAt) 623 - return err 624 - } 625 - 626 - func (db *DB) DeleteLike(uri string) error { 627 - _, err := db.Exec(db.Rebind(`DELETE FROM likes WHERE uri = ?`), uri) 628 - return err 629 - } 630 - 631 - func (db *DB) GetLikeCount(subjectURI string) (int, error) { 632 - var count int 633 - err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM likes WHERE subject_uri = ?`), subjectURI).Scan(&count) 634 - return count, err 635 - } 636 - 637 - func (db *DB) GetReplyCount(rootURI string) (int, error) { 638 - var count int 639 - err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM replies WHERE root_uri = ?`), rootURI).Scan(&count) 640 - return count, err 641 - } 642 - 643 - func (db *DB) GetLikeByUserAndSubject(userDID, subjectURI string) (*Like, error) { 644 - var like Like 645 - err := db.QueryRow(db.Rebind(` 646 - SELECT uri, author_did, subject_uri, created_at, indexed_at 647 - FROM likes 648 - WHERE author_did = ? AND subject_uri = ? 649 - `), userDID, subjectURI).Scan(&like.URI, &like.AuthorDID, &like.SubjectURI, &like.CreatedAt, &like.IndexedAt) 650 - if err != nil { 651 - return nil, err 652 - } 653 - return &like, nil 654 - } 655 - 656 - func (db *DB) CreateCollection(c *Collection) error { 657 - _, err := db.Exec(db.Rebind(` 658 - INSERT INTO collections (uri, author_did, name, description, icon, created_at, indexed_at) 659 - VALUES (?, ?, ?, ?, ?, ?, ?) 660 - ON CONFLICT(uri) DO UPDATE SET 661 - name = excluded.name, 662 - description = excluded.description, 663 - icon = excluded.icon, 664 - indexed_at = excluded.indexed_at 665 - `), c.URI, c.AuthorDID, c.Name, c.Description, c.Icon, c.CreatedAt, c.IndexedAt) 666 - return err 667 - } 668 - 669 - func (db *DB) GetCollectionsByAuthor(authorDID string) ([]Collection, error) { 670 - rows, err := db.Query(db.Rebind(` 671 - SELECT uri, author_did, name, description, icon, created_at, indexed_at 672 - FROM collections 673 - WHERE author_did = ? 674 - ORDER BY created_at DESC 675 - `), authorDID) 676 - if err != nil { 677 - return nil, err 678 - } 679 - defer rows.Close() 680 - 681 - var collections []Collection 682 - for rows.Next() { 683 - var c Collection 684 - if err := rows.Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt); err != nil { 685 - return nil, err 686 - } 687 - collections = append(collections, c) 688 - } 689 - return collections, nil 690 - } 691 - 692 - func (db *DB) GetCollectionByURI(uri string) (*Collection, error) { 693 - var c Collection 694 - err := db.QueryRow(db.Rebind(` 695 - SELECT uri, author_did, name, description, icon, created_at, indexed_at 696 - FROM collections 697 - WHERE uri = ? 698 - `), uri).Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt) 699 - if err != nil { 700 - return nil, err 701 - } 702 - return &c, nil 703 - } 704 - 705 - func (db *DB) DeleteCollection(uri string) error { 706 - 707 - db.Exec(db.Rebind(`DELETE FROM collection_items WHERE collection_uri = ?`), uri) 708 - _, err := db.Exec(db.Rebind(`DELETE FROM collections WHERE uri = ?`), uri) 709 - return err 710 - } 711 - 712 - func (db *DB) AddToCollection(item *CollectionItem) error { 713 - _, err := db.Exec(db.Rebind(` 714 - INSERT INTO collection_items (uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at) 715 - VALUES (?, ?, ?, ?, ?, ?, ?) 716 - ON CONFLICT(uri) DO UPDATE SET 717 - position = excluded.position, 718 - indexed_at = excluded.indexed_at 719 - `), item.URI, item.AuthorDID, item.CollectionURI, item.AnnotationURI, item.Position, item.CreatedAt, item.IndexedAt) 720 - return err 721 - } 722 - 723 - func (db *DB) GetCollectionItems(collectionURI string) ([]CollectionItem, error) { 724 - rows, err := db.Query(db.Rebind(` 725 - SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at 726 - FROM collection_items 727 - WHERE collection_uri = ? 728 - ORDER BY position ASC, created_at DESC 729 - `), collectionURI) 730 - if err != nil { 731 - return nil, err 732 - } 733 - defer rows.Close() 734 - 735 - var items []CollectionItem 736 - for rows.Next() { 737 - var item CollectionItem 738 - if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil { 739 - return nil, err 740 - } 741 - items = append(items, item) 742 - } 743 - return items, nil 744 - } 745 - 746 - func (db *DB) RemoveFromCollection(uri string) error { 747 - _, err := db.Exec(db.Rebind(`DELETE FROM collection_items WHERE uri = ?`), uri) 748 - return err 749 - } 750 - 751 - func (db *DB) GetRecentCollectionItems(limit, offset int) ([]CollectionItem, error) { 752 - rows, err := db.Query(db.Rebind(` 753 - SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at 754 - FROM collection_items 755 - ORDER BY created_at DESC 756 - LIMIT ? OFFSET ? 757 - `), limit, offset) 758 - if err != nil { 759 - return nil, err 760 - } 761 - defer rows.Close() 762 - 763 - var items []CollectionItem 764 - for rows.Next() { 765 - var item CollectionItem 766 - if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil { 767 - return nil, err 768 - } 769 - items = append(items, item) 770 - } 771 - return items, nil 772 - } 773 - 774 - func (db *DB) GetCollectionURIsForAnnotation(annotationURI string) ([]string, error) { 775 - rows, err := db.Query(db.Rebind(` 776 - SELECT collection_uri FROM collection_items WHERE annotation_uri = ? 777 - `), annotationURI) 778 - if err != nil { 779 - return nil, err 780 - } 781 - defer rows.Close() 782 - 783 - var uris []string 784 - for rows.Next() { 785 - var uri string 786 - if err := rows.Scan(&uri); err != nil { 787 - return nil, err 788 - } 789 - uris = append(uris, uri) 790 - } 791 - return uris, nil 792 - } 793 - 794 - func (db *DB) SaveSession(id, did, handle, accessToken, refreshToken, dpopKey string, expiresAt time.Time) error { 795 - _, err := db.Exec(db.Rebind(` 796 - INSERT INTO sessions (id, did, handle, access_token, refresh_token, dpop_key, created_at, expires_at) 797 - VALUES (?, ?, ?, ?, ?, ?, ?, ?) 798 - ON CONFLICT(id) DO UPDATE SET 799 - access_token = excluded.access_token, 800 - refresh_token = excluded.refresh_token, 801 - dpop_key = excluded.dpop_key, 802 - expires_at = excluded.expires_at 803 - `), id, did, handle, accessToken, refreshToken, dpopKey, time.Now(), expiresAt) 804 - return err 805 - } 806 - 807 - func (db *DB) GetSession(id string) (did, handle, accessToken, refreshToken, dpopKey string, err error) { 808 - err = db.QueryRow(db.Rebind(` 809 - SELECT did, handle, access_token, refresh_token, COALESCE(dpop_key, '') 810 - FROM sessions 811 - WHERE id = ? AND expires_at > ? 812 - `), id, time.Now()).Scan(&did, &handle, &accessToken, &refreshToken, &dpopKey) 813 - return 814 - } 815 - 816 - func (db *DB) DeleteSession(id string) error { 817 - _, err := db.Exec(db.Rebind(`DELETE FROM sessions WHERE id = ?`), id) 818 - return err 819 - } 820 - 821 43 func HashURL(rawURL string) string { 822 44 parsed, err := url.Parse(rawURL) 823 45 if err != nil { ··· 825 47 } 826 48 827 49 normalized := strings.ToLower(parsed.Host) + parsed.Path 50 + if parsed.RawQuery != "" { 51 + normalized += "?" + parsed.RawQuery 52 + } 828 53 normalized = strings.TrimSuffix(normalized, "/") 829 54 830 55 return hashString(normalized) ··· 841 66 return string(b) 842 67 } 843 68 844 - func (db *DB) CreateNotification(n *Notification) error { 845 - _, err := db.Exec(db.Rebind(` 846 - INSERT INTO notifications (recipient_did, actor_did, type, subject_uri, created_at) 847 - VALUES (?, ?, ?, ?, ?) 848 - `), n.RecipientDID, n.ActorDID, n.Type, n.SubjectURI, n.CreatedAt) 849 - return err 850 - } 851 - 852 - func (db *DB) GetNotifications(recipientDID string, limit, offset int) ([]Notification, error) { 853 - rows, err := db.Query(db.Rebind(` 854 - SELECT id, recipient_did, actor_did, type, subject_uri, created_at, read_at 855 - FROM notifications 856 - WHERE recipient_did = ? 857 - ORDER BY created_at DESC 858 - LIMIT ? OFFSET ? 859 - `), recipientDID, limit, offset) 860 - if err != nil { 861 - return nil, err 862 - } 863 - defer rows.Close() 864 - 865 - var notifications []Notification 866 - for rows.Next() { 867 - var n Notification 868 - if err := rows.Scan(&n.ID, &n.RecipientDID, &n.ActorDID, &n.Type, &n.SubjectURI, &n.CreatedAt, &n.ReadAt); err != nil { 869 - continue 870 - } 871 - notifications = append(notifications, n) 872 - } 873 - return notifications, nil 874 - } 875 - 876 - func (db *DB) GetUnreadNotificationCount(recipientDID string) (int, error) { 877 - var count int 878 - err := db.QueryRow(db.Rebind(` 879 - SELECT COUNT(*) FROM notifications WHERE recipient_did = ? AND read_at IS NULL 880 - `), recipientDID).Scan(&count) 881 - return count, err 882 - } 883 - 884 - func (db *DB) MarkNotificationsRead(recipientDID string) error { 885 - _, err := db.Exec(db.Rebind(` 886 - UPDATE notifications SET read_at = ? WHERE recipient_did = ? AND read_at IS NULL 887 - `), time.Now(), recipientDID) 888 - return err 889 - } 890 - 891 69 func (db *DB) GetAuthorByURI(uri string) (string, error) { 892 70 var authorDID string 893 71 err := db.QueryRow(db.Rebind(`SELECT author_did FROM annotations WHERE uri = ?`), uri).Scan(&authorDID) ··· 907 85 908 86 return "", fmt.Errorf("uri not found or no author") 909 87 } 88 + 89 + func buildPlaceholders(n int) string { 90 + if n == 0 { 91 + return "" 92 + } 93 + placeholders := make([]string, n) 94 + for i := range placeholders { 95 + placeholders[i] = "?" 96 + } 97 + return strings.Join(placeholders, ", ") 98 + }
+172
backend/internal/db/queries_annotations.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) CreateAnnotation(a *Annotation) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO annotations (uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid) 10 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 11 + ON CONFLICT(uri) DO UPDATE SET 12 + motivation = excluded.motivation, 13 + body_value = excluded.body_value, 14 + body_format = excluded.body_format, 15 + body_uri = excluded.body_uri, 16 + target_title = excluded.target_title, 17 + selector_json = excluded.selector_json, 18 + tags_json = excluded.tags_json, 19 + indexed_at = excluded.indexed_at, 20 + cid = excluded.cid 21 + `), a.URI, a.AuthorDID, a.Motivation, a.BodyValue, a.BodyFormat, a.BodyURI, a.TargetSource, a.TargetHash, a.TargetTitle, a.SelectorJSON, a.TagsJSON, a.CreatedAt, a.IndexedAt, a.CID) 22 + return err 23 + } 24 + 25 + func (db *DB) GetAnnotationByURI(uri string) (*Annotation, error) { 26 + var a Annotation 27 + err := db.QueryRow(db.Rebind(` 28 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 29 + FROM annotations 30 + WHERE uri = ? 31 + `), uri).Scan(&a.URI, &a.AuthorDID, &a.Motivation, &a.BodyValue, &a.BodyFormat, &a.BodyURI, &a.TargetSource, &a.TargetHash, &a.TargetTitle, &a.SelectorJSON, &a.TagsJSON, &a.CreatedAt, &a.IndexedAt, &a.CID) 32 + if err != nil { 33 + return nil, err 34 + } 35 + return &a, nil 36 + } 37 + 38 + func (db *DB) GetAnnotationsByTargetHash(targetHash string, limit, offset int) ([]Annotation, error) { 39 + rows, err := db.Query(db.Rebind(` 40 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 41 + FROM annotations 42 + WHERE target_hash = ? 43 + ORDER BY created_at DESC 44 + LIMIT ? OFFSET ? 45 + `), targetHash, limit, offset) 46 + if err != nil { 47 + return nil, err 48 + } 49 + defer rows.Close() 50 + 51 + return scanAnnotations(rows) 52 + } 53 + 54 + func (db *DB) GetAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) { 55 + rows, err := db.Query(db.Rebind(` 56 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 57 + FROM annotations 58 + WHERE author_did = ? 59 + ORDER BY created_at DESC 60 + LIMIT ? OFFSET ? 61 + `), authorDID, limit, offset) 62 + if err != nil { 63 + return nil, err 64 + } 65 + defer rows.Close() 66 + 67 + return scanAnnotations(rows) 68 + } 69 + 70 + func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) { 71 + rows, err := db.Query(db.Rebind(` 72 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 73 + FROM annotations 74 + WHERE motivation = ? 75 + ORDER BY created_at DESC 76 + LIMIT ? OFFSET ? 77 + `), motivation, limit, offset) 78 + if err != nil { 79 + return nil, err 80 + } 81 + defer rows.Close() 82 + 83 + return scanAnnotations(rows) 84 + } 85 + 86 + func (db *DB) GetRecentAnnotations(limit, offset int) ([]Annotation, error) { 87 + rows, err := db.Query(db.Rebind(` 88 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 89 + FROM annotations 90 + ORDER BY created_at DESC 91 + LIMIT ? OFFSET ? 92 + `), limit, offset) 93 + if err != nil { 94 + return nil, err 95 + } 96 + defer rows.Close() 97 + 98 + return scanAnnotations(rows) 99 + } 100 + 101 + func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) { 102 + pattern := "%\"" + tag + "\"%" 103 + rows, err := db.Query(db.Rebind(` 104 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 105 + FROM annotations 106 + WHERE tags_json LIKE ? 107 + ORDER BY created_at DESC 108 + LIMIT ? OFFSET ? 109 + `), pattern, limit, offset) 110 + if err != nil { 111 + return nil, err 112 + } 113 + defer rows.Close() 114 + 115 + return scanAnnotations(rows) 116 + } 117 + 118 + func (db *DB) DeleteAnnotation(uri string) error { 119 + _, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri) 120 + return err 121 + } 122 + 123 + func (db *DB) UpdateAnnotation(uri, bodyValue, tagsJSON, cid string) error { 124 + _, err := db.Exec(db.Rebind(` 125 + UPDATE annotations 126 + SET body_value = ?, tags_json = ?, cid = ?, indexed_at = ? 127 + WHERE uri = ? 128 + `), bodyValue, tagsJSON, cid, time.Now(), uri) 129 + return err 130 + } 131 + 132 + func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) { 133 + pattern := "%\"" + tag + "\"%" 134 + rows, err := db.Query(db.Rebind(` 135 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 136 + FROM annotations 137 + WHERE author_did = ? AND tags_json LIKE ? 138 + ORDER BY created_at DESC 139 + LIMIT ? OFFSET ? 140 + `), authorDID, pattern, limit, offset) 141 + if err != nil { 142 + return nil, err 143 + } 144 + defer rows.Close() 145 + 146 + return scanAnnotations(rows) 147 + } 148 + 149 + func (db *DB) GetAnnotationsByURIs(uris []string) ([]Annotation, error) { 150 + if len(uris) == 0 { 151 + return []Annotation{}, nil 152 + } 153 + 154 + query := db.Rebind(` 155 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 156 + FROM annotations 157 + WHERE uri IN (` + buildPlaceholders(len(uris)) + `) 158 + `) 159 + 160 + args := make([]interface{}, len(uris)) 161 + for i, uri := range uris { 162 + args[i] = uri 163 + } 164 + 165 + rows, err := db.Query(query, args...) 166 + if err != nil { 167 + return nil, err 168 + } 169 + defer rows.Close() 170 + 171 + return scanAnnotations(rows) 172 + }
+176
backend/internal/db/queries_bookmarks.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) CreateBookmark(b *Bookmark) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO bookmarks (uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid) 10 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 11 + ON CONFLICT(uri) DO UPDATE SET 12 + title = excluded.title, 13 + description = excluded.description, 14 + tags_json = excluded.tags_json, 15 + indexed_at = excluded.indexed_at, 16 + cid = excluded.cid 17 + `), b.URI, b.AuthorDID, b.Source, b.SourceHash, b.Title, b.Description, b.TagsJSON, b.CreatedAt, b.IndexedAt, b.CID) 18 + return err 19 + } 20 + 21 + func (db *DB) GetBookmarkByURI(uri string) (*Bookmark, error) { 22 + var b Bookmark 23 + err := db.QueryRow(db.Rebind(` 24 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 25 + FROM bookmarks 26 + WHERE uri = ? 27 + `), uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID) 28 + if err != nil { 29 + return nil, err 30 + } 31 + return &b, nil 32 + } 33 + 34 + func (db *DB) GetRecentBookmarks(limit, offset int) ([]Bookmark, error) { 35 + rows, err := db.Query(db.Rebind(` 36 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 37 + FROM bookmarks 38 + ORDER BY created_at DESC 39 + LIMIT ? OFFSET ? 40 + `), limit, offset) 41 + if err != nil { 42 + return nil, err 43 + } 44 + defer rows.Close() 45 + 46 + var bookmarks []Bookmark 47 + for rows.Next() { 48 + var b Bookmark 49 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 50 + return nil, err 51 + } 52 + bookmarks = append(bookmarks, b) 53 + } 54 + return bookmarks, nil 55 + } 56 + 57 + func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) { 58 + pattern := "%\"" + tag + "\"%" 59 + rows, err := db.Query(db.Rebind(` 60 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 61 + FROM bookmarks 62 + WHERE tags_json LIKE ? 63 + ORDER BY created_at DESC 64 + LIMIT ? OFFSET ? 65 + `), pattern, limit, offset) 66 + if err != nil { 67 + return nil, err 68 + } 69 + defer rows.Close() 70 + 71 + var bookmarks []Bookmark 72 + for rows.Next() { 73 + var b Bookmark 74 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 75 + return nil, err 76 + } 77 + bookmarks = append(bookmarks, b) 78 + } 79 + return bookmarks, nil 80 + } 81 + 82 + func (db *DB) GetBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) { 83 + pattern := "%\"" + tag + "\"%" 84 + rows, err := db.Query(db.Rebind(` 85 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 86 + FROM bookmarks 87 + WHERE author_did = ? AND tags_json LIKE ? 88 + ORDER BY created_at DESC 89 + LIMIT ? OFFSET ? 90 + `), authorDID, pattern, limit, offset) 91 + if err != nil { 92 + return nil, err 93 + } 94 + defer rows.Close() 95 + 96 + var bookmarks []Bookmark 97 + for rows.Next() { 98 + var b Bookmark 99 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 100 + return nil, err 101 + } 102 + bookmarks = append(bookmarks, b) 103 + } 104 + return bookmarks, nil 105 + } 106 + 107 + func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) { 108 + rows, err := db.Query(db.Rebind(` 109 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 110 + FROM bookmarks 111 + WHERE author_did = ? 112 + ORDER BY created_at DESC 113 + LIMIT ? OFFSET ? 114 + `), authorDID, limit, offset) 115 + if err != nil { 116 + return nil, err 117 + } 118 + defer rows.Close() 119 + 120 + var bookmarks []Bookmark 121 + for rows.Next() { 122 + var b Bookmark 123 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 124 + return nil, err 125 + } 126 + bookmarks = append(bookmarks, b) 127 + } 128 + return bookmarks, nil 129 + } 130 + 131 + func (db *DB) DeleteBookmark(uri string) error { 132 + _, err := db.Exec(db.Rebind(`DELETE FROM bookmarks WHERE uri = ?`), uri) 133 + return err 134 + } 135 + 136 + func (db *DB) UpdateBookmark(uri, title, description, tagsJSON, cid string) error { 137 + _, err := db.Exec(db.Rebind(` 138 + UPDATE bookmarks 139 + SET title = ?, description = ?, tags_json = ?, cid = ?, indexed_at = ? 140 + WHERE uri = ? 141 + `), title, description, tagsJSON, cid, time.Now(), uri) 142 + return err 143 + } 144 + 145 + func (db *DB) GetBookmarksByURIs(uris []string) ([]Bookmark, error) { 146 + if len(uris) == 0 { 147 + return []Bookmark{}, nil 148 + } 149 + 150 + query := db.Rebind(` 151 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 152 + FROM bookmarks 153 + WHERE uri IN (` + buildPlaceholders(len(uris)) + `) 154 + `) 155 + 156 + args := make([]interface{}, len(uris)) 157 + for i, uri := range uris { 158 + args[i] = uri 159 + } 160 + 161 + rows, err := db.Query(query, args...) 162 + if err != nil { 163 + return nil, err 164 + } 165 + defer rows.Close() 166 + 167 + var bookmarks []Bookmark 168 + for rows.Next() { 169 + var b Bookmark 170 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 171 + return nil, err 172 + } 173 + bookmarks = append(bookmarks, b) 174 + } 175 + return bookmarks, nil 176 + }
+172
backend/internal/db/queries_collections.go
··· 1 + package db 2 + 3 + func (db *DB) CreateCollection(c *Collection) error { 4 + _, err := db.Exec(db.Rebind(` 5 + INSERT INTO collections (uri, author_did, name, description, icon, created_at, indexed_at) 6 + VALUES (?, ?, ?, ?, ?, ?, ?) 7 + ON CONFLICT(uri) DO UPDATE SET 8 + name = excluded.name, 9 + description = excluded.description, 10 + icon = excluded.icon, 11 + indexed_at = excluded.indexed_at 12 + `), c.URI, c.AuthorDID, c.Name, c.Description, c.Icon, c.CreatedAt, c.IndexedAt) 13 + return err 14 + } 15 + 16 + func (db *DB) GetCollectionsByAuthor(authorDID string) ([]Collection, error) { 17 + rows, err := db.Query(db.Rebind(` 18 + SELECT uri, author_did, name, description, icon, created_at, indexed_at 19 + FROM collections 20 + WHERE author_did = ? 21 + ORDER BY created_at DESC 22 + `), authorDID) 23 + if err != nil { 24 + return nil, err 25 + } 26 + defer rows.Close() 27 + 28 + var collections []Collection 29 + for rows.Next() { 30 + var c Collection 31 + if err := rows.Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt); err != nil { 32 + return nil, err 33 + } 34 + collections = append(collections, c) 35 + } 36 + return collections, nil 37 + } 38 + 39 + func (db *DB) GetCollectionByURI(uri string) (*Collection, error) { 40 + var c Collection 41 + err := db.QueryRow(db.Rebind(` 42 + SELECT uri, author_did, name, description, icon, created_at, indexed_at 43 + FROM collections 44 + WHERE uri = ? 45 + `), uri).Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt) 46 + if err != nil { 47 + return nil, err 48 + } 49 + return &c, nil 50 + } 51 + 52 + func (db *DB) DeleteCollection(uri string) error { 53 + 54 + db.Exec(db.Rebind(`DELETE FROM collection_items WHERE collection_uri = ?`), uri) 55 + _, err := db.Exec(db.Rebind(`DELETE FROM collections WHERE uri = ?`), uri) 56 + return err 57 + } 58 + 59 + func (db *DB) AddToCollection(item *CollectionItem) error { 60 + _, err := db.Exec(db.Rebind(` 61 + INSERT INTO collection_items (uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at) 62 + VALUES (?, ?, ?, ?, ?, ?, ?) 63 + ON CONFLICT(uri) DO UPDATE SET 64 + position = excluded.position, 65 + indexed_at = excluded.indexed_at 66 + `), item.URI, item.AuthorDID, item.CollectionURI, item.AnnotationURI, item.Position, item.CreatedAt, item.IndexedAt) 67 + return err 68 + } 69 + 70 + func (db *DB) GetCollectionItems(collectionURI string) ([]CollectionItem, error) { 71 + rows, err := db.Query(db.Rebind(` 72 + SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at 73 + FROM collection_items 74 + WHERE collection_uri = ? 75 + ORDER BY position ASC, created_at DESC 76 + `), collectionURI) 77 + if err != nil { 78 + return nil, err 79 + } 80 + defer rows.Close() 81 + 82 + var items []CollectionItem 83 + for rows.Next() { 84 + var item CollectionItem 85 + if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil { 86 + return nil, err 87 + } 88 + items = append(items, item) 89 + } 90 + return items, nil 91 + } 92 + 93 + func (db *DB) RemoveFromCollection(uri string) error { 94 + _, err := db.Exec(db.Rebind(`DELETE FROM collection_items WHERE uri = ?`), uri) 95 + return err 96 + } 97 + 98 + func (db *DB) GetRecentCollectionItems(limit, offset int) ([]CollectionItem, error) { 99 + rows, err := db.Query(db.Rebind(` 100 + SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at 101 + FROM collection_items 102 + ORDER BY created_at DESC 103 + LIMIT ? OFFSET ? 104 + `), limit, offset) 105 + if err != nil { 106 + return nil, err 107 + } 108 + defer rows.Close() 109 + 110 + var items []CollectionItem 111 + for rows.Next() { 112 + var item CollectionItem 113 + if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil { 114 + return nil, err 115 + } 116 + items = append(items, item) 117 + } 118 + return items, nil 119 + } 120 + 121 + func (db *DB) GetCollectionURIsForAnnotation(annotationURI string) ([]string, error) { 122 + rows, err := db.Query(db.Rebind(` 123 + SELECT collection_uri FROM collection_items WHERE annotation_uri = ? 124 + `), annotationURI) 125 + if err != nil { 126 + return nil, err 127 + } 128 + defer rows.Close() 129 + 130 + var uris []string 131 + for rows.Next() { 132 + var uri string 133 + if err := rows.Scan(&uri); err != nil { 134 + return nil, err 135 + } 136 + uris = append(uris, uri) 137 + } 138 + return uris, nil 139 + } 140 + 141 + func (db *DB) GetCollectionsByURIs(uris []string) ([]Collection, error) { 142 + if len(uris) == 0 { 143 + return []Collection{}, nil 144 + } 145 + 146 + query := db.Rebind(` 147 + SELECT uri, author_did, name, description, icon, created_at, indexed_at 148 + FROM collections 149 + WHERE uri IN (` + buildPlaceholders(len(uris)) + `) 150 + `) 151 + 152 + args := make([]interface{}, len(uris)) 153 + for i, uri := range uris { 154 + args[i] = uri 155 + } 156 + 157 + rows, err := db.Query(query, args...) 158 + if err != nil { 159 + return nil, err 160 + } 161 + defer rows.Close() 162 + 163 + var collections []Collection 164 + for rows.Next() { 165 + var c Collection 166 + if err := rows.Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt); err != nil { 167 + return nil, err 168 + } 169 + collections = append(collections, c) 170 + } 171 + return collections, nil 172 + }
+201
backend/internal/db/queries_highlights.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) CreateHighlight(h *Highlight) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO highlights (uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid) 10 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 11 + ON CONFLICT(uri) DO UPDATE SET 12 + target_title = excluded.target_title, 13 + selector_json = excluded.selector_json, 14 + color = excluded.color, 15 + tags_json = excluded.tags_json, 16 + indexed_at = excluded.indexed_at, 17 + cid = excluded.cid 18 + `), h.URI, h.AuthorDID, h.TargetSource, h.TargetHash, h.TargetTitle, h.SelectorJSON, h.Color, h.TagsJSON, h.CreatedAt, h.IndexedAt, h.CID) 19 + return err 20 + } 21 + 22 + func (db *DB) GetHighlightByURI(uri string) (*Highlight, error) { 23 + var h Highlight 24 + err := db.QueryRow(db.Rebind(` 25 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 26 + FROM highlights 27 + WHERE uri = ? 28 + `), uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID) 29 + if err != nil { 30 + return nil, err 31 + } 32 + return &h, nil 33 + } 34 + 35 + func (db *DB) GetRecentHighlights(limit, offset int) ([]Highlight, error) { 36 + rows, err := db.Query(db.Rebind(` 37 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 38 + FROM highlights 39 + ORDER BY created_at DESC 40 + LIMIT ? OFFSET ? 41 + `), limit, offset) 42 + if err != nil { 43 + return nil, err 44 + } 45 + defer rows.Close() 46 + 47 + var highlights []Highlight 48 + for rows.Next() { 49 + var h Highlight 50 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 51 + return nil, err 52 + } 53 + highlights = append(highlights, h) 54 + } 55 + return highlights, nil 56 + } 57 + 58 + func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) { 59 + pattern := "%\"" + tag + "\"%" 60 + rows, err := db.Query(db.Rebind(` 61 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 62 + FROM highlights 63 + WHERE tags_json LIKE ? 64 + ORDER BY created_at DESC 65 + LIMIT ? OFFSET ? 66 + `), pattern, limit, offset) 67 + if err != nil { 68 + return nil, err 69 + } 70 + defer rows.Close() 71 + 72 + var highlights []Highlight 73 + for rows.Next() { 74 + var h Highlight 75 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 76 + return nil, err 77 + } 78 + highlights = append(highlights, h) 79 + } 80 + return highlights, nil 81 + } 82 + 83 + func (db *DB) GetHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) { 84 + pattern := "%\"" + tag + "\"%" 85 + rows, err := db.Query(db.Rebind(` 86 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 87 + FROM highlights 88 + WHERE author_did = ? AND tags_json LIKE ? 89 + ORDER BY created_at DESC 90 + LIMIT ? OFFSET ? 91 + `), authorDID, pattern, limit, offset) 92 + if err != nil { 93 + return nil, err 94 + } 95 + defer rows.Close() 96 + 97 + var highlights []Highlight 98 + for rows.Next() { 99 + var h Highlight 100 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 101 + return nil, err 102 + } 103 + highlights = append(highlights, h) 104 + } 105 + return highlights, nil 106 + } 107 + 108 + func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) { 109 + rows, err := db.Query(db.Rebind(` 110 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 111 + FROM highlights 112 + WHERE target_hash = ? 113 + ORDER BY created_at DESC 114 + LIMIT ? OFFSET ? 115 + `), targetHash, limit, offset) 116 + if err != nil { 117 + return nil, err 118 + } 119 + defer rows.Close() 120 + 121 + var highlights []Highlight 122 + for rows.Next() { 123 + var h Highlight 124 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 125 + return nil, err 126 + } 127 + highlights = append(highlights, h) 128 + } 129 + return highlights, nil 130 + } 131 + 132 + func (db *DB) GetHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) { 133 + rows, err := db.Query(db.Rebind(` 134 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 135 + FROM highlights 136 + WHERE author_did = ? 137 + ORDER BY created_at DESC 138 + LIMIT ? OFFSET ? 139 + `), authorDID, limit, offset) 140 + if err != nil { 141 + return nil, err 142 + } 143 + defer rows.Close() 144 + 145 + var highlights []Highlight 146 + for rows.Next() { 147 + var h Highlight 148 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 149 + return nil, err 150 + } 151 + highlights = append(highlights, h) 152 + } 153 + return highlights, nil 154 + } 155 + 156 + func (db *DB) DeleteHighlight(uri string) error { 157 + _, err := db.Exec(db.Rebind(`DELETE FROM highlights WHERE uri = ?`), uri) 158 + return err 159 + } 160 + 161 + func (db *DB) UpdateHighlight(uri, color, tagsJSON, cid string) error { 162 + _, err := db.Exec(db.Rebind(` 163 + UPDATE highlights 164 + SET color = ?, tags_json = ?, cid = ?, indexed_at = ? 165 + WHERE uri = ? 166 + `), color, tagsJSON, cid, time.Now(), uri) 167 + return err 168 + } 169 + 170 + func (db *DB) GetHighlightsByURIs(uris []string) ([]Highlight, error) { 171 + if len(uris) == 0 { 172 + return []Highlight{}, nil 173 + } 174 + 175 + query := db.Rebind(` 176 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 177 + FROM highlights 178 + WHERE uri IN (` + buildPlaceholders(len(uris)) + `) 179 + `) 180 + 181 + args := make([]interface{}, len(uris)) 182 + for i, uri := range uris { 183 + args[i] = uri 184 + } 185 + 186 + rows, err := db.Query(query, args...) 187 + if err != nil { 188 + return nil, err 189 + } 190 + defer rows.Close() 191 + 192 + var highlights []Highlight 193 + for rows.Next() { 194 + var h Highlight 195 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 196 + return nil, err 197 + } 198 + highlights = append(highlights, h) 199 + } 200 + return highlights, nil 201 + }
+36
backend/internal/db/queries_history.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) SaveEditHistory(uri, recordType, previousContent string, previousCID *string) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO edit_history (uri, record_type, previous_content, previous_cid, edited_at) 10 + VALUES (?, ?, ?, ?, ?) 11 + `), uri, recordType, previousContent, previousCID, time.Now()) 12 + return err 13 + } 14 + 15 + func (db *DB) GetEditHistory(uri string) ([]EditHistory, error) { 16 + rows, err := db.Query(db.Rebind(` 17 + SELECT id, uri, record_type, previous_content, previous_cid, edited_at 18 + FROM edit_history 19 + WHERE uri = ? 20 + ORDER BY edited_at DESC 21 + `), uri) 22 + if err != nil { 23 + return nil, err 24 + } 25 + defer rows.Close() 26 + 27 + var history []EditHistory 28 + for rows.Next() { 29 + var h EditHistory 30 + if err := rows.Scan(&h.ID, &h.URI, &h.RecordType, &h.PreviousContent, &h.PreviousCID, &h.EditedAt); err != nil { 31 + return nil, err 32 + } 33 + history = append(history, h) 34 + } 35 + return history, nil 36 + }
+59
backend/internal/db/queries_keys.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) CreateAPIKey(key *APIKey) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO api_keys (id, owner_did, name, key_hash, created_at) 10 + VALUES (?, ?, ?, ?, ?) 11 + `), key.ID, key.OwnerDID, key.Name, key.KeyHash, key.CreatedAt) 12 + return err 13 + } 14 + 15 + func (db *DB) GetAPIKeysByOwner(ownerDID string) ([]APIKey, error) { 16 + rows, err := db.Query(db.Rebind(` 17 + SELECT id, owner_did, name, key_hash, created_at, last_used_at 18 + FROM api_keys 19 + WHERE owner_did = ? 20 + ORDER BY created_at DESC 21 + `), ownerDID) 22 + if err != nil { 23 + return nil, err 24 + } 25 + defer rows.Close() 26 + 27 + var keys []APIKey 28 + for rows.Next() { 29 + var k APIKey 30 + if err := rows.Scan(&k.ID, &k.OwnerDID, &k.Name, &k.KeyHash, &k.CreatedAt, &k.LastUsedAt); err != nil { 31 + return nil, err 32 + } 33 + keys = append(keys, k) 34 + } 35 + return keys, nil 36 + } 37 + 38 + func (db *DB) GetAPIKeyByHash(keyHash string) (*APIKey, error) { 39 + var k APIKey 40 + err := db.QueryRow(db.Rebind(` 41 + SELECT id, owner_did, name, key_hash, created_at, last_used_at 42 + FROM api_keys 43 + WHERE key_hash = ? 44 + `), keyHash).Scan(&k.ID, &k.OwnerDID, &k.Name, &k.KeyHash, &k.CreatedAt, &k.LastUsedAt) 45 + if err != nil { 46 + return nil, err 47 + } 48 + return &k, nil 49 + } 50 + 51 + func (db *DB) DeleteAPIKey(id, ownerDID string) error { 52 + _, err := db.Exec(db.Rebind(`DELETE FROM api_keys WHERE id = ? AND owner_did = ?`), id, ownerDID) 53 + return err 54 + } 55 + 56 + func (db *DB) UpdateAPIKeyLastUsed(id string) error { 57 + _, err := db.Exec(db.Rebind(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`), time.Now(), id) 58 + return err 59 + }
+105
backend/internal/db/queries_likes.go
··· 1 + package db 2 + 3 + func (db *DB) CreateLike(l *Like) error { 4 + _, err := db.Exec(db.Rebind(` 5 + INSERT INTO likes (uri, author_did, subject_uri, created_at, indexed_at) 6 + VALUES (?, ?, ?, ?, ?) 7 + ON CONFLICT(uri) DO NOTHING 8 + `), l.URI, l.AuthorDID, l.SubjectURI, l.CreatedAt, l.IndexedAt) 9 + return err 10 + } 11 + 12 + func (db *DB) DeleteLike(uri string) error { 13 + _, err := db.Exec(db.Rebind(`DELETE FROM likes WHERE uri = ?`), uri) 14 + return err 15 + } 16 + 17 + func (db *DB) GetLikeCount(subjectURI string) (int, error) { 18 + var count int 19 + err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM likes WHERE subject_uri = ?`), subjectURI).Scan(&count) 20 + return count, err 21 + } 22 + 23 + func (db *DB) GetLikeByUserAndSubject(userDID, subjectURI string) (*Like, error) { 24 + var like Like 25 + err := db.QueryRow(db.Rebind(` 26 + SELECT uri, author_did, subject_uri, created_at, indexed_at 27 + FROM likes 28 + WHERE author_did = ? AND subject_uri = ? 29 + `), userDID, subjectURI).Scan(&like.URI, &like.AuthorDID, &like.SubjectURI, &like.CreatedAt, &like.IndexedAt) 30 + if err != nil { 31 + return nil, err 32 + } 33 + return &like, nil 34 + } 35 + 36 + func (db *DB) GetLikeCounts(subjectURIs []string) (map[string]int, error) { 37 + if len(subjectURIs) == 0 { 38 + return map[string]int{}, nil 39 + } 40 + 41 + query := db.Rebind(` 42 + SELECT subject_uri, COUNT(*) 43 + FROM likes 44 + WHERE subject_uri IN (` + buildPlaceholders(len(subjectURIs)) + `) 45 + GROUP BY subject_uri 46 + `) 47 + 48 + args := make([]interface{}, len(subjectURIs)) 49 + for i, uri := range subjectURIs { 50 + args[i] = uri 51 + } 52 + 53 + rows, err := db.Query(query, args...) 54 + if err != nil { 55 + return nil, err 56 + } 57 + defer rows.Close() 58 + 59 + counts := make(map[string]int) 60 + for rows.Next() { 61 + var uri string 62 + var count int 63 + if err := rows.Scan(&uri, &count); err != nil { 64 + return nil, err 65 + } 66 + counts[uri] = count 67 + } 68 + 69 + return counts, nil 70 + } 71 + 72 + func (db *DB) GetViewerLikes(viewerDID string, subjectURIs []string) (map[string]bool, error) { 73 + if len(subjectURIs) == 0 { 74 + return map[string]bool{}, nil 75 + } 76 + 77 + query := db.Rebind(` 78 + SELECT subject_uri 79 + FROM likes 80 + WHERE author_did = ? AND subject_uri IN (` + buildPlaceholders(len(subjectURIs)) + `) 81 + `) 82 + 83 + args := make([]interface{}, len(subjectURIs)+1) 84 + args[0] = viewerDID 85 + for i, uri := range subjectURIs { 86 + args[i+1] = uri 87 + } 88 + 89 + rows, err := db.Query(query, args...) 90 + if err != nil { 91 + return nil, err 92 + } 93 + defer rows.Close() 94 + 95 + likes := make(map[string]bool) 96 + for rows.Next() { 97 + var uri string 98 + if err := rows.Scan(&uri); err != nil { 99 + return nil, err 100 + } 101 + likes[uri] = true 102 + } 103 + 104 + return likes, nil 105 + }
+52
backend/internal/db/queries_notifications.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) CreateNotification(n *Notification) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO notifications (recipient_did, actor_did, type, subject_uri, created_at) 10 + VALUES (?, ?, ?, ?, ?) 11 + `), n.RecipientDID, n.ActorDID, n.Type, n.SubjectURI, n.CreatedAt) 12 + return err 13 + } 14 + 15 + func (db *DB) GetNotifications(recipientDID string, limit, offset int) ([]Notification, error) { 16 + rows, err := db.Query(db.Rebind(` 17 + SELECT id, recipient_did, actor_did, type, subject_uri, created_at, read_at 18 + FROM notifications 19 + WHERE recipient_did = ? 20 + ORDER BY created_at DESC 21 + LIMIT ? OFFSET ? 22 + `), recipientDID, limit, offset) 23 + if err != nil { 24 + return nil, err 25 + } 26 + defer rows.Close() 27 + 28 + var notifications []Notification 29 + for rows.Next() { 30 + var n Notification 31 + if err := rows.Scan(&n.ID, &n.RecipientDID, &n.ActorDID, &n.Type, &n.SubjectURI, &n.CreatedAt, &n.ReadAt); err != nil { 32 + continue 33 + } 34 + notifications = append(notifications, n) 35 + } 36 + return notifications, nil 37 + } 38 + 39 + func (db *DB) GetUnreadNotificationCount(recipientDID string) (int, error) { 40 + var count int 41 + err := db.QueryRow(db.Rebind(` 42 + SELECT COUNT(*) FROM notifications WHERE recipient_did = ? AND read_at IS NULL 43 + `), recipientDID).Scan(&count) 44 + return count, err 45 + } 46 + 47 + func (db *DB) MarkNotificationsRead(recipientDID string) error { 48 + _, err := db.Exec(db.Rebind(` 49 + UPDATE notifications SET read_at = ? WHERE recipient_did = ? AND read_at IS NULL 50 + `), time.Now(), recipientDID) 51 + return err 52 + }
+176
backend/internal/db/queries_replies.go
··· 1 + package db 2 + 3 + func (db *DB) CreateReply(r *Reply) error { 4 + _, err := db.Exec(db.Rebind(` 5 + INSERT INTO replies (uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid) 6 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 7 + ON CONFLICT(uri) DO UPDATE SET 8 + text = excluded.text, 9 + format = excluded.format, 10 + indexed_at = excluded.indexed_at, 11 + cid = excluded.cid 12 + `), r.URI, r.AuthorDID, r.ParentURI, r.RootURI, r.Text, r.Format, r.CreatedAt, r.IndexedAt, r.CID) 13 + return err 14 + } 15 + 16 + func (db *DB) GetRepliesByRoot(rootURI string) ([]Reply, error) { 17 + rows, err := db.Query(db.Rebind(` 18 + SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 19 + FROM replies 20 + WHERE root_uri = ? 21 + ORDER BY created_at ASC 22 + `), rootURI) 23 + if err != nil { 24 + return nil, err 25 + } 26 + defer rows.Close() 27 + 28 + var replies []Reply 29 + for rows.Next() { 30 + var r Reply 31 + if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 32 + return nil, err 33 + } 34 + replies = append(replies, r) 35 + } 36 + return replies, nil 37 + } 38 + 39 + func (db *DB) GetReplyByURI(uri string) (*Reply, error) { 40 + var r Reply 41 + err := db.QueryRow(db.Rebind(` 42 + SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 43 + FROM replies 44 + WHERE uri = ? 45 + `), uri).Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID) 46 + if err != nil { 47 + return nil, err 48 + } 49 + return &r, nil 50 + } 51 + 52 + func (db *DB) DeleteReply(uri string) error { 53 + _, err := db.Exec(db.Rebind(`DELETE FROM replies WHERE uri = ?`), uri) 54 + return err 55 + } 56 + 57 + func (db *DB) GetRepliesByAuthor(authorDID string) ([]Reply, error) { 58 + rows, err := db.Query(db.Rebind(` 59 + SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 60 + FROM replies 61 + WHERE author_did = ? 62 + ORDER BY created_at DESC 63 + `), authorDID) 64 + if err != nil { 65 + return nil, err 66 + } 67 + defer rows.Close() 68 + 69 + var replies []Reply 70 + for rows.Next() { 71 + var r Reply 72 + if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 73 + return nil, err 74 + } 75 + replies = append(replies, r) 76 + } 77 + return replies, nil 78 + } 79 + 80 + func (db *DB) GetOrphanedRepliesByAuthor(authorDID string) ([]Reply, error) { 81 + rows, err := db.Query(db.Rebind(` 82 + SELECT r.uri, r.author_did, r.parent_uri, r.root_uri, r.text, r.format, r.created_at, r.indexed_at, r.cid 83 + FROM replies r 84 + LEFT JOIN annotations a ON r.root_uri = a.uri 85 + WHERE r.author_did = ? AND a.uri IS NULL 86 + `), authorDID) 87 + if err != nil { 88 + return nil, err 89 + } 90 + defer rows.Close() 91 + 92 + var replies []Reply 93 + for rows.Next() { 94 + var r Reply 95 + if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 96 + return nil, err 97 + } 98 + replies = append(replies, r) 99 + } 100 + return replies, nil 101 + } 102 + 103 + func (db *DB) GetReplyCount(rootURI string) (int, error) { 104 + var count int 105 + err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM replies WHERE root_uri = ?`), rootURI).Scan(&count) 106 + return count, err 107 + } 108 + 109 + func (db *DB) GetReplyCounts(rootURIs []string) (map[string]int, error) { 110 + if len(rootURIs) == 0 { 111 + return map[string]int{}, nil 112 + } 113 + 114 + query := db.Rebind(` 115 + SELECT root_uri, COUNT(*) 116 + FROM replies 117 + WHERE root_uri IN (` + buildPlaceholders(len(rootURIs)) + `) 118 + GROUP BY root_uri 119 + `) 120 + 121 + args := make([]interface{}, len(rootURIs)) 122 + for i, uri := range rootURIs { 123 + args[i] = uri 124 + } 125 + 126 + rows, err := db.Query(query, args...) 127 + if err != nil { 128 + return nil, err 129 + } 130 + defer rows.Close() 131 + 132 + counts := make(map[string]int) 133 + for rows.Next() { 134 + var uri string 135 + var count int 136 + if err := rows.Scan(&uri, &count); err != nil { 137 + return nil, err 138 + } 139 + counts[uri] = count 140 + } 141 + 142 + return counts, nil 143 + } 144 + 145 + func (db *DB) GetRepliesByURIs(uris []string) ([]Reply, error) { 146 + if len(uris) == 0 { 147 + return []Reply{}, nil 148 + } 149 + 150 + query := db.Rebind(` 151 + SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid 152 + FROM replies 153 + WHERE uri IN (` + buildPlaceholders(len(uris)) + `) 154 + `) 155 + 156 + args := make([]interface{}, len(uris)) 157 + for i, uri := range uris { 158 + args[i] = uri 159 + } 160 + 161 + rows, err := db.Query(query, args...) 162 + if err != nil { 163 + return nil, err 164 + } 165 + defer rows.Close() 166 + 167 + var replies []Reply 168 + for rows.Next() { 169 + var r Reply 170 + if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil { 171 + return nil, err 172 + } 173 + replies = append(replies, r) 174 + } 175 + return replies, nil 176 + }
+32
backend/internal/db/queries_sessions.go
··· 1 + package db 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + func (db *DB) SaveSession(id, did, handle, accessToken, refreshToken, dpopKey string, expiresAt time.Time) error { 8 + _, err := db.Exec(db.Rebind(` 9 + INSERT INTO sessions (id, did, handle, access_token, refresh_token, dpop_key, created_at, expires_at) 10 + VALUES (?, ?, ?, ?, ?, ?, ?, ?) 11 + ON CONFLICT(id) DO UPDATE SET 12 + access_token = excluded.access_token, 13 + refresh_token = excluded.refresh_token, 14 + dpop_key = excluded.dpop_key, 15 + expires_at = excluded.expires_at 16 + `), id, did, handle, accessToken, refreshToken, dpopKey, time.Now(), expiresAt) 17 + return err 18 + } 19 + 20 + func (db *DB) GetSession(id string) (did, handle, accessToken, refreshToken, dpopKey string, err error) { 21 + err = db.QueryRow(db.Rebind(` 22 + SELECT did, handle, access_token, refresh_token, COALESCE(dpop_key, '') 23 + FROM sessions 24 + WHERE id = ? AND expires_at > ? 25 + `), id, time.Now()).Scan(&did, &handle, &accessToken, &refreshToken, &dpopKey) 26 + return 27 + } 28 + 29 + func (db *DB) DeleteSession(id string) error { 30 + _, err := db.Exec(db.Rebind(`DELETE FROM sessions WHERE id = ?`), id) 31 + return err 32 + }
+236 -84
backend/internal/firehose/ingester.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "encoding/binary" 6 7 "encoding/json" 7 8 "fmt" 8 9 "io" 9 10 "log" 10 - "net/http" 11 - "strings" 12 11 "time" 12 + 13 + "github.com/fxamacker/cbor/v2" 14 + "github.com/gorilla/websocket" 15 + "github.com/ipfs/go-cid" 13 16 14 17 "margin.at/internal/db" 15 18 ) ··· 56 59 return 57 60 default: 58 61 if err := i.subscribe(ctx); err != nil { 62 + log.Printf("Firehose error: %v, reconnecting in 5s...", err) 59 63 if ctx.Err() != nil { 60 64 return 61 65 } 62 - time.Sleep(30 * time.Second) 66 + time.Sleep(5 * time.Second) 63 67 } 64 68 } 65 69 } 66 70 } 67 71 72 + type FrameHeader struct { 73 + Op int `cbor:"op"` 74 + T string `cbor:"t"` 75 + } 76 + type Commit struct { 77 + Repo string `cbor:"repo"` 78 + Rev string `cbor:"rev"` 79 + Seq int64 `cbor:"seq"` 80 + Prev *cid.Cid `cbor:"prev"` 81 + Time string `cbor:"time"` 82 + Blocks []byte `cbor:"blocks"` 83 + Ops []RepoOp `cbor:"ops"` 84 + } 85 + 86 + type RepoOp struct { 87 + Action string `cbor:"action"` 88 + Path string `cbor:"path"` 89 + Cid *cid.Cid `cbor:"cid"` 90 + } 91 + 68 92 func (i *Ingester) subscribe(ctx context.Context) error { 69 93 cursor := i.getLastCursor() 70 94 ··· 73 97 url = fmt.Sprintf("%s?cursor=%d", RelayURL, cursor) 74 98 } 75 99 76 - req, err := http.NewRequestWithContext(ctx, "GET", strings.Replace(url, "wss://", "https://", 1), nil) 77 - if err != nil { 78 - return err 79 - } 100 + log.Printf("Connecting to firehose: %s", url) 80 101 81 - resp, err := http.DefaultClient.Do(req) 102 + conn, _, err := websocket.DefaultDialer.DialContext(ctx, url, nil) 82 103 if err != nil { 83 - return err 104 + return fmt.Errorf("websocket dial failed: %w", err) 84 105 } 85 - defer resp.Body.Close() 106 + defer conn.Close() 86 107 87 - if resp.StatusCode != 200 { 88 - body, _ := io.ReadAll(resp.Body) 89 - return fmt.Errorf("firehose returned %d: %s", resp.StatusCode, string(body)) 90 - } 108 + log.Printf("Connected to firehose") 91 109 92 - decoder := json.NewDecoder(resp.Body) 93 110 for { 94 111 select { 95 112 case <-ctx.Done(): ··· 97 114 default: 98 115 } 99 116 100 - var event FirehoseEvent 101 - if err := decoder.Decode(&event); err != nil { 102 - if err == io.EOF { 103 - return nil 104 - } 105 - return err 117 + _, message, err := conn.ReadMessage() 118 + if err != nil { 119 + return fmt.Errorf("websocket read failed: %w", err) 106 120 } 107 121 108 - i.handleEvent(&event) 122 + i.handleMessage(message) 109 123 } 110 124 } 111 125 112 - type FirehoseEvent struct { 113 - Repo string `json:"repo"` 114 - Collection string `json:"collection"` 115 - Rkey string `json:"rkey"` 116 - Record json.RawMessage `json:"record"` 117 - Operation string `json:"operation"` 118 - Cursor int64 `json:"cursor"` 119 - } 126 + func (i *Ingester) handleMessage(data []byte) { 127 + reader := bytes.NewReader(data) 120 128 121 - func (i *Ingester) handleEvent(event *FirehoseEvent) { 122 - uri := fmt.Sprintf("at://%s/%s/%s", event.Repo, event.Collection, event.Rkey) 129 + var header FrameHeader 130 + decoder := cbor.NewDecoder(reader) 131 + if err := decoder.Decode(&header); err != nil { 132 + return 133 + } 123 134 124 - switch event.Collection { 125 - case CollectionAnnotation: 126 - switch event.Operation { 127 - case "create", "update": 128 - i.handleAnnotation(event) 129 - case "delete": 130 - i.db.DeleteAnnotation(uri) 135 + if header.Op != 1 { 136 + return 137 + } 138 + 139 + if header.T != "#commit" { 140 + return 141 + } 142 + 143 + var commit Commit 144 + if err := decoder.Decode(&commit); err != nil { 145 + return 146 + } 147 + 148 + for _, op := range commit.Ops { 149 + collection, rkey := parseOpPath(op.Path) 150 + if !isMarginCollection(collection) { 151 + continue 131 152 } 132 - case CollectionHighlight: 133 - switch event.Operation { 153 + 154 + uri := fmt.Sprintf("at://%s/%s/%s", commit.Repo, collection, rkey) 155 + 156 + switch op.Action { 134 157 case "create", "update": 135 - i.handleHighlight(event) 158 + if op.Cid != nil && len(commit.Blocks) > 0 { 159 + record := extractRecord(commit.Blocks, *op.Cid) 160 + if record != nil { 161 + i.handleRecord(commit.Repo, collection, rkey, record, commit.Seq) 162 + } 163 + } 136 164 case "delete": 137 - i.db.DeleteHighlight(uri) 165 + i.handleDelete(collection, uri) 138 166 } 139 - case CollectionBookmark: 140 - switch event.Operation { 141 - case "create", "update": 142 - i.handleBookmark(event) 143 - case "delete": 144 - i.db.DeleteBookmark(uri) 167 + } 168 + 169 + if commit.Seq > 0 { 170 + if err := i.db.SetCursor("firehose_cursor", commit.Seq); err != nil { 171 + log.Printf("Failed to save cursor: %v", err) 145 172 } 146 - case CollectionReply: 147 - switch event.Operation { 148 - case "create", "update": 149 - i.handleReply(event) 150 - case "delete": 151 - i.db.DeleteReply(uri) 173 + } 174 + } 175 + 176 + func parseOpPath(path string) (collection, rkey string) { 177 + for i := len(path) - 1; i >= 0; i-- { 178 + if path[i] == '/' { 179 + return path[:i], path[i+1:] 152 180 } 153 - case CollectionLike: 154 - switch event.Operation { 155 - case "create": 156 - i.handleLike(event) 157 - case "delete": 158 - i.db.DeleteLike(uri) 181 + } 182 + return path, "" 183 + } 184 + 185 + func isMarginCollection(collection string) bool { 186 + switch collection { 187 + case CollectionAnnotation, CollectionHighlight, CollectionBookmark, 188 + CollectionReply, CollectionLike, CollectionCollection, CollectionCollectionItem: 189 + return true 190 + } 191 + return false 192 + } 193 + 194 + func extractRecord(blocks []byte, targetCid cid.Cid) map[string]interface{} { 195 + reader := bytes.NewReader(blocks) 196 + 197 + headerLen, err := binary.ReadUvarint(reader) 198 + if err != nil { 199 + return nil 200 + } 201 + reader.Seek(int64(headerLen), io.SeekCurrent) 202 + 203 + for reader.Len() > 0 { 204 + blockLen, err := binary.ReadUvarint(reader) 205 + if err != nil { 206 + break 159 207 } 160 - case CollectionCollection: 161 - switch event.Operation { 162 - case "create", "update": 163 - i.handleCollection(event) 164 - case "delete": 165 - i.db.DeleteCollection(uri) 208 + 209 + blockData := make([]byte, blockLen) 210 + if _, err := io.ReadFull(reader, blockData); err != nil { 211 + break 166 212 } 167 - case CollectionCollectionItem: 168 - switch event.Operation { 169 - case "create", "update": 170 - i.handleCollectionItem(event) 171 - case "delete": 172 - i.db.RemoveFromCollection(uri) 213 + 214 + blockCid, cidLen, err := parseCidFromBlock(blockData) 215 + if err != nil { 216 + continue 217 + } 218 + 219 + if blockCid.Equals(targetCid) { 220 + var record map[string]interface{} 221 + if err := cbor.Unmarshal(blockData[cidLen:], &record); err != nil { 222 + return nil 223 + } 224 + return record 173 225 } 174 226 } 175 227 176 - if event.Cursor > 0 { 177 - if err := i.db.SetCursor("firehose_cursor", event.Cursor); err != nil { 178 - log.Printf("Failed to save cursor: %v", err) 228 + return nil 229 + } 230 + 231 + func parseCidFromBlock(data []byte) (cid.Cid, int, error) { 232 + if len(data) < 2 { 233 + return cid.Cid{}, 0, fmt.Errorf("data too short") 234 + } 235 + version, n1 := binary.Uvarint(data) 236 + if n1 <= 0 { 237 + return cid.Cid{}, 0, fmt.Errorf("invalid version varint") 238 + } 239 + 240 + if version == 1 { 241 + codec, n2 := binary.Uvarint(data[n1:]) 242 + if n2 <= 0 { 243 + return cid.Cid{}, 0, fmt.Errorf("invalid codec varint") 179 244 } 245 + 246 + mhStart := n1 + n2 247 + hashType, n3 := binary.Uvarint(data[mhStart:]) 248 + if n3 <= 0 { 249 + return cid.Cid{}, 0, fmt.Errorf("invalid hash type varint") 250 + } 251 + 252 + hashLen, n4 := binary.Uvarint(data[mhStart+n3:]) 253 + if n4 <= 0 { 254 + return cid.Cid{}, 0, fmt.Errorf("invalid hash length varint") 255 + } 256 + 257 + totalCidLen := mhStart + n3 + n4 + int(hashLen) 258 + 259 + c, err := cid.Cast(data[:totalCidLen]) 260 + if err != nil { 261 + return cid.Cid{}, 0, err 262 + } 263 + 264 + _ = codec 265 + _ = hashType 266 + 267 + return c, totalCidLen, nil 180 268 } 269 + 270 + return cid.Cid{}, 0, fmt.Errorf("unsupported CID version") 181 271 } 182 272 183 - func (i *Ingester) handleAnnotation(event *FirehoseEvent) { 273 + func (i *Ingester) handleDelete(collection, uri string) { 274 + switch collection { 275 + case CollectionAnnotation: 276 + i.db.DeleteAnnotation(uri) 277 + case CollectionHighlight: 278 + i.db.DeleteHighlight(uri) 279 + case CollectionBookmark: 280 + i.db.DeleteBookmark(uri) 281 + case CollectionReply: 282 + i.db.DeleteReply(uri) 283 + case CollectionLike: 284 + i.db.DeleteLike(uri) 285 + case CollectionCollection: 286 + i.db.DeleteCollection(uri) 287 + case CollectionCollectionItem: 288 + i.db.RemoveFromCollection(uri) 289 + } 290 + } 184 291 292 + func (i *Ingester) handleRecord(repo, collection, rkey string, record map[string]interface{}, seq int64) { 293 + _ = fmt.Sprintf("at://%s/%s/%s", repo, collection, rkey) 294 + 295 + recordJSON, err := json.Marshal(record) 296 + if err != nil { 297 + return 298 + } 299 + 300 + event := &FirehoseEvent{ 301 + Repo: repo, 302 + Collection: collection, 303 + Rkey: rkey, 304 + Record: recordJSON, 305 + Operation: "create", 306 + Cursor: seq, 307 + } 308 + 309 + switch collection { 310 + case CollectionAnnotation: 311 + i.handleAnnotation(event) 312 + case CollectionHighlight: 313 + i.handleHighlight(event) 314 + case CollectionBookmark: 315 + i.handleBookmark(event) 316 + case CollectionReply: 317 + i.handleReply(event) 318 + case CollectionLike: 319 + i.handleLike(event) 320 + case CollectionCollection: 321 + i.handleCollection(event) 322 + case CollectionCollectionItem: 323 + i.handleCollectionItem(event) 324 + } 325 + } 326 + 327 + type FirehoseEvent struct { 328 + Repo string `json:"repo"` 329 + Collection string `json:"collection"` 330 + Rkey string `json:"rkey"` 331 + Record json.RawMessage `json:"record"` 332 + Operation string `json:"operation"` 333 + Cursor int64 `json:"cursor"` 334 + } 335 + 336 + func (i *Ingester) handleAnnotation(event *FirehoseEvent) { 185 337 var record struct { 186 338 Motivation string `json:"motivation"` 187 339 Body struct { ··· 205 357 Title string `json:"title"` 206 358 } 207 359 208 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 360 + if err := json.Unmarshal(event.Record, &record); err != nil { 209 361 return 210 362 } 211 363 ··· 302 454 CreatedAt string `json:"createdAt"` 303 455 } 304 456 305 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 457 + if err := json.Unmarshal(event.Record, &record); err != nil { 306 458 return 307 459 } 308 460 ··· 334 486 CreatedAt string `json:"createdAt"` 335 487 } 336 488 337 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 489 + if err := json.Unmarshal(event.Record, &record); err != nil { 338 490 return 339 491 } 340 492 ··· 369 521 CreatedAt string `json:"createdAt"` 370 522 } 371 523 372 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 524 + if err := json.Unmarshal(event.Record, &record); err != nil { 373 525 return 374 526 } 375 527 ··· 432 584 CreatedAt string `json:"createdAt"` 433 585 } 434 586 435 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 587 + if err := json.Unmarshal(event.Record, &record); err != nil { 436 588 return 437 589 } 438 590 ··· 488 640 CreatedAt string `json:"createdAt"` 489 641 } 490 642 491 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 643 + if err := json.Unmarshal(event.Record, &record); err != nil { 492 644 return 493 645 } 494 646 ··· 532 684 CreatedAt string `json:"createdAt"` 533 685 } 534 686 535 - if err := json.NewDecoder(bytes.NewReader(event.Record)).Decode(&record); err != nil { 687 + if err := json.Unmarshal(event.Record, &record); err != nil { 536 688 return 537 689 } 538 690
+2 -1
docker-compose.yml
··· 9 9 - OAUTH_KEY_PATH=/data/oauth_private_key.pem 10 10 env_file: 11 11 - .env 12 + volumes: 13 + - margin-data:/data 12 14 depends_on: 13 15 db: 14 16 condition: service_healthy ··· 23 25 24 26 volumes: 25 27 - db-data:/var/lib/postgresql/data 26 - - margin-data:/data 27 28 healthcheck: 28 29 test: ["CMD-SHELL", "pg_isready -U margin"] 29 30 interval: 5s
+98 -49
extension/background/service-worker.js
··· 6 6 const hasSidebarAction = 7 7 typeof browser !== "undefined" && 8 8 typeof browser.sidebarAction !== "undefined"; 9 - const hasSessionStorage = 10 - typeof chrome !== "undefined" && 11 - chrome.storage && 12 - typeof chrome.storage.session !== "undefined"; 13 9 const hasNotifications = 14 10 typeof chrome !== "undefined" && typeof chrome.notifications !== "undefined"; 15 11 ··· 43 39 } 44 40 } 45 41 46 - async function openAnnotationUI(tabId) { 42 + async function openAnnotationUI(tabId, windowId) { 47 43 if (hasSidePanel) { 48 44 try { 49 - const tab = await chrome.tabs.get(tabId); 50 - await chrome.sidePanel.setOptions({ 51 - tabId: tabId, 52 - path: "sidepanel/sidepanel.html", 53 - enabled: true, 54 - }); 55 - await chrome.sidePanel.open({ windowId: tab.windowId }); 45 + let targetWindowId = windowId; 46 + 47 + if (!targetWindowId) { 48 + const tab = await chrome.tabs.get(tabId); 49 + targetWindowId = tab.windowId; 50 + } 51 + 52 + await chrome.sidePanel.open({ windowId: targetWindowId }); 56 53 return true; 57 54 } catch (err) { 58 55 console.error("Could not open Chrome side panel:", err); ··· 71 68 return false; 72 69 } 73 70 74 - async function storePendingAnnotation(data) { 75 - if (hasSessionStorage) { 76 - await chrome.storage.session.set({ pendingAnnotation: data }); 77 - } else { 78 - await chrome.storage.local.set({ 79 - pendingAnnotation: data, 80 - pendingAnnotationExpiry: Date.now() + 60000, 81 - }); 82 - } 83 - } 84 - 85 71 chrome.runtime.onInstalled.addListener(async () => { 86 72 const stored = await chrome.storage.local.get(["apiUrl"]); 87 73 if (!stored.apiUrl) { ··· 118 104 if (hasSidebarAction) { 119 105 try { 120 106 await browser.sidebarAction.close(); 121 - } catch (e) {} 107 + } catch { 108 + /* ignore */ 109 + } 122 110 } 123 111 }); 124 112 125 - chrome.action.onClicked.addListener(async (tab) => { 113 + chrome.action.onClicked.addListener(async () => { 126 114 const stored = await chrome.storage.local.get(["apiUrl"]); 127 115 const webUrl = stored.apiUrl || WEB_BASE; 128 116 chrome.tabs.create({ url: webUrl }); ··· 130 118 131 119 chrome.contextMenus.onClicked.addListener(async (info, tab) => { 132 120 if (info.menuItemId === "margin-open-sidebar") { 133 - if (hasSidePanel && chrome.sidePanel && chrome.sidePanel.open) { 134 - try { 135 - await chrome.sidePanel.open({ windowId: tab.windowId }); 136 - } catch (err) { 137 - console.error("Failed to open side panel:", err); 138 - } 139 - } else if (hasSidebarAction) { 140 - try { 141 - await browser.sidebarAction.open(); 142 - } catch (err) { 143 - console.error("Failed to open Firefox sidebar:", err); 144 - } 145 - } 121 + await openAnnotationUI(tab.id, tab.windowId); 146 122 return; 147 123 } 148 124 ··· 189 165 selectionText: info.selectionText, 190 166 }); 191 167 selector = response?.selector; 192 - } catch (err) {} 193 - 194 - if (selector && (hasSidePanel || hasSidebarAction)) { 195 - await storePendingAnnotation({ 196 - url: tab.url, 197 - title: tab.title, 198 - selector: selector, 199 - }); 200 - const opened = await openAnnotationUI(tab.id); 201 - if (opened) return; 168 + } catch { 169 + /* ignore */ 202 170 } 203 171 204 172 if (!selector && info.selectionText) { ··· 208 176 }; 209 177 } 210 178 179 + if (selector) { 180 + try { 181 + await chrome.tabs.sendMessage(tab.id, { 182 + type: "SHOW_INLINE_ANNOTATE", 183 + data: { 184 + url: tab.url, 185 + title: tab.title, 186 + selector: selector, 187 + }, 188 + }); 189 + return; 190 + } catch (e) { 191 + console.debug("Inline annotate failed, falling back to new tab:", e); 192 + } 193 + } 194 + 211 195 if (WEB_BASE) { 212 196 let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(tab.url)}`; 213 197 if (selector) { ··· 227 211 selectionText: info.selectionText, 228 212 }); 229 213 if (response && response.success) return; 230 - } catch (err) {} 214 + } catch { 215 + /* ignore */ 216 + } 231 217 232 218 if (info.selectionText) { 233 219 selector = { ··· 657 643 throw new Error( 658 644 `Failed to add to collection: ${res.status} ${errText}`, 659 645 ); 646 + } 647 + 648 + const data = await res.json(); 649 + sendResponse({ success: true, data }); 650 + break; 651 + } 652 + 653 + case "GET_REPLIES": { 654 + if (!API_BASE) { 655 + sendResponse({ success: false, error: "API URL not configured" }); 656 + return; 657 + } 658 + 659 + const uri = request.data.uri; 660 + const res = await fetch( 661 + `${API_BASE}/api/replies?uri=${encodeURIComponent(uri)}`, 662 + ); 663 + 664 + if (!res.ok) { 665 + throw new Error(`Failed to fetch replies: ${res.status}`); 666 + } 667 + 668 + const data = await res.json(); 669 + sendResponse({ success: true, data: data.items || [] }); 670 + break; 671 + } 672 + 673 + case "CREATE_REPLY": { 674 + if (!API_BASE) { 675 + sendResponse({ success: false, error: "API URL not configured" }); 676 + return; 677 + } 678 + 679 + const cookie = await chrome.cookies.get({ 680 + url: API_BASE, 681 + name: "margin_session", 682 + }); 683 + 684 + if (!cookie) { 685 + sendResponse({ success: false, error: "Not authenticated" }); 686 + return; 687 + } 688 + 689 + const { parentUri, parentCid, rootUri, rootCid, text } = request.data; 690 + const res = await fetch(`${API_BASE}/api/annotations/reply`, { 691 + method: "POST", 692 + credentials: "include", 693 + headers: { 694 + "Content-Type": "application/json", 695 + "X-Session-Token": cookie.value, 696 + }, 697 + body: JSON.stringify({ 698 + parentUri, 699 + parentCid, 700 + rootUri, 701 + rootCid, 702 + text, 703 + }), 704 + }); 705 + 706 + if (!res.ok) { 707 + const errText = await res.text(); 708 + throw new Error(`Failed to create reply: ${res.status} ${errText}`); 660 709 } 661 710 662 711 const data = await res.json();
+584 -214
extension/content/content.js
··· 4 4 let popoverEl = null; 5 5 6 6 let activeItems = []; 7 - 8 - let hoveredItems = []; 9 - let tooltipEl = null; 10 - let hideTimer = null; 7 + let currentSelection = null; 11 8 12 9 const OVERLAY_STYLES = ` 13 10 :host { all: initial; } ··· 18 15 width: 100%; 19 16 height: 100%; 20 17 pointer-events: none; 21 - } 22 - .margin-badge { 23 - position: absolute; 24 - background: #6366f1; 25 - color: white; 26 - padding: 4px 10px; 27 - border-radius: 99px; 28 - font-size: 11px; 29 - font-weight: 600; 30 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 31 - cursor: pointer; 32 - pointer-events: auto; 33 - box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); 34 - display: flex; 35 - align-items: center; 36 - gap: 6px; 37 - transform: translateY(-120%); 38 - white-space: nowrap; 39 - transition: transform 0.15s, background-color 0.15s; 40 - z-index: 2147483647; 41 - } 42 - .margin-badge:hover { 43 - transform: translateY(-125%) scale(1.05); 44 - background: #4f46e5; 45 - z-index: 2147483647; 46 - } 47 - .margin-badge-avatar { 48 - width: 16px; 49 - height: 16px; 50 - border-radius: 50%; 51 - background: rgba(255,255,255,0.2); 52 - display: flex; 53 - align-items: center; 54 - justify-content: center; 55 - font-size: 9px; 56 - object-fit: cover; 57 - } 58 - .margin-badge-stack { 59 - display: flex; 60 - align-items: center; 61 - } 62 - .margin-badge-stack .margin-badge-avatar { 63 - margin-left: -6px; 64 - border: 1px solid #6366f1; 65 - } 66 - .margin-badge-stack .margin-badge-avatar:first-child { 67 - margin-left: 0; 68 - } 69 - .margin-badge-stem { 70 - position: absolute; 71 - left: 14px; 72 - bottom: -6px; 73 - width: 2px; 74 - height: 6px; 75 - background: #6366f1; 76 - border-radius: 2px; 77 18 } 78 19 79 20 .margin-popover { ··· 149 90 padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; 150 91 } 151 92 .btn-action:hover { background: #27272a; color: #e4e4e7; } 93 + 94 + .margin-selection-popup { 95 + position: fixed; 96 + display: flex; 97 + gap: 4px; 98 + padding: 6px; 99 + background: #09090b; 100 + border: 1px solid #27272a; 101 + border-radius: 8px; 102 + box-shadow: 0 8px 16px rgba(0,0,0,0.4); 103 + z-index: 2147483647; 104 + pointer-events: auto; 105 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 106 + animation: popover-in 0.15s forwards; 107 + } 108 + .selection-btn { 109 + display: flex; 110 + align-items: center; 111 + gap: 6px; 112 + padding: 6px 12px; 113 + background: transparent; 114 + border: none; 115 + border-radius: 6px; 116 + color: #e4e4e7; 117 + font-size: 12px; 118 + font-weight: 500; 119 + cursor: pointer; 120 + transition: background 0.15s; 121 + } 122 + .selection-btn:hover { 123 + background: #27272a; 124 + } 125 + .selection-btn svg { 126 + width: 14px; 127 + height: 14px; 128 + } 129 + .inline-compose-modal { 130 + position: fixed; 131 + width: 340px; 132 + max-width: calc(100vw - 40px); 133 + background: #09090b; 134 + border: 1px solid #27272a; 135 + border-radius: 12px; 136 + padding: 16px; 137 + box-sizing: border-box; 138 + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); 139 + z-index: 2147483647; 140 + pointer-events: auto; 141 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 142 + color: #e4e4e7; 143 + animation: popover-in 0.15s forwards; 144 + overflow: hidden; 145 + } 146 + .inline-compose-modal * { 147 + box-sizing: border-box; 148 + } 149 + .inline-compose-quote { 150 + padding: 8px 12px; 151 + background: #18181b; 152 + border-left: 3px solid #6366f1; 153 + border-radius: 4px; 154 + font-size: 12px; 155 + color: #a1a1aa; 156 + font-style: italic; 157 + margin-bottom: 12px; 158 + max-height: 60px; 159 + overflow: hidden; 160 + word-break: break-word; 161 + } 162 + .inline-compose-textarea { 163 + width: 100%; 164 + min-height: 80px; 165 + padding: 10px 12px; 166 + background: #18181b; 167 + border: 1px solid #27272a; 168 + border-radius: 8px; 169 + color: #e4e4e7; 170 + font-family: inherit; 171 + font-size: 13px; 172 + resize: vertical; 173 + margin-bottom: 12px; 174 + box-sizing: border-box; 175 + } 176 + .inline-compose-textarea:focus { 177 + outline: none; 178 + border-color: #6366f1; 179 + } 180 + .inline-compose-actions { 181 + display: flex; 182 + justify-content: flex-end; 183 + gap: 8px; 184 + } 185 + .btn-cancel { 186 + padding: 8px 16px; 187 + background: transparent; 188 + border: 1px solid #27272a; 189 + border-radius: 6px; 190 + color: #a1a1aa; 191 + font-size: 13px; 192 + cursor: pointer; 193 + } 194 + .btn-cancel:hover { 195 + background: #27272a; 196 + color: #e4e4e7; 197 + } 198 + .btn-submit { 199 + padding: 8px 16px; 200 + background: #6366f1; 201 + border: none; 202 + border-radius: 6px; 203 + color: white; 204 + font-size: 13px; 205 + font-weight: 500; 206 + cursor: pointer; 207 + } 208 + .btn-submit:hover { 209 + background: #4f46e5; 210 + } 211 + .btn-submit:disabled { 212 + opacity: 0.5; 213 + cursor: not-allowed; 214 + } 215 + .reply-section { 216 + border-top: 1px solid #27272a; 217 + padding: 12px 16px; 218 + background: #0f0f12; 219 + border-radius: 0 0 12px 12px; 220 + } 221 + .reply-textarea { 222 + width: 100%; 223 + min-height: 60px; 224 + padding: 8px 10px; 225 + background: #18181b; 226 + border: 1px solid #27272a; 227 + border-radius: 6px; 228 + color: #e4e4e7; 229 + font-family: inherit; 230 + font-size: 12px; 231 + resize: none; 232 + margin-bottom: 8px; 233 + } 234 + .reply-textarea:focus { 235 + outline: none; 236 + border-color: #6366f1; 237 + } 238 + .reply-submit { 239 + padding: 6px 12px; 240 + background: #6366f1; 241 + border: none; 242 + border-radius: 4px; 243 + color: white; 244 + font-size: 11px; 245 + font-weight: 500; 246 + cursor: pointer; 247 + float: right; 248 + } 249 + .reply-submit:disabled { 250 + opacity: 0.5; 251 + } 252 + .reply-item { 253 + padding: 8px 0; 254 + border-top: 1px solid #27272a; 255 + } 256 + .reply-item:first-child { 257 + border-top: none; 258 + } 259 + .reply-author { 260 + font-size: 11px; 261 + font-weight: 600; 262 + color: #a1a1aa; 263 + margin-bottom: 4px; 264 + } 265 + .reply-text { 266 + font-size: 12px; 267 + color: #e4e4e7; 268 + line-height: 1.4; 269 + } 152 270 `; 153 271 154 272 class DOMTextMatcher { ··· 203 321 let matchIndex = this.corpus.indexOf(searchText); 204 322 205 323 if (matchIndex === -1) { 206 - const cleaned = searchText.replace(/\s+/g, " "); 207 - return null; 324 + const normalizedSearch = searchText.replace(/\s+/g, " ").trim(); 325 + matchIndex = this.corpus.indexOf(normalizedSearch); 326 + 327 + if (matchIndex === -1) { 328 + const fuzzyMatch = this.fuzzyFindInCorpus(searchText); 329 + if (fuzzyMatch) { 330 + const start = this.mapIndexToPoint(fuzzyMatch.start); 331 + const end = this.mapIndexToPoint(fuzzyMatch.end); 332 + if (start && end) { 333 + const range = document.createRange(); 334 + range.setStart(start.node, start.offset); 335 + range.setEnd(end.node, end.offset); 336 + return range; 337 + } 338 + } 339 + return null; 340 + } 208 341 } 209 342 210 343 const start = this.mapIndexToPoint(matchIndex); ··· 219 352 return null; 220 353 } 221 354 355 + fuzzyFindInCorpus(searchText) { 356 + const searchWords = searchText 357 + .trim() 358 + .split(/\s+/) 359 + .filter((w) => w.length > 0); 360 + if (searchWords.length === 0) return null; 361 + 362 + const corpusLower = this.corpus.toLowerCase(); 363 + 364 + const firstWord = searchWords[0].toLowerCase(); 365 + let searchStart = 0; 366 + 367 + while (searchStart < corpusLower.length) { 368 + const wordStart = corpusLower.indexOf(firstWord, searchStart); 369 + if (wordStart === -1) break; 370 + 371 + let corpusPos = wordStart; 372 + let matched = true; 373 + let lastMatchEnd = wordStart; 374 + 375 + for (const word of searchWords) { 376 + const wordLower = word.toLowerCase(); 377 + while ( 378 + corpusPos < corpusLower.length && 379 + /\s/.test(this.corpus[corpusPos]) 380 + ) { 381 + corpusPos++; 382 + } 383 + const corpusSlice = corpusLower.slice( 384 + corpusPos, 385 + corpusPos + wordLower.length, 386 + ); 387 + if (corpusSlice !== wordLower) { 388 + matched = false; 389 + break; 390 + } 391 + 392 + corpusPos += wordLower.length; 393 + lastMatchEnd = corpusPos; 394 + } 395 + 396 + if (matched) { 397 + return { start: wordStart, end: lastMatchEnd }; 398 + } 399 + 400 + searchStart = wordStart + 1; 401 + } 402 + 403 + return null; 404 + } 405 + 222 406 mapIndexToPoint(corpusIndex) { 223 407 for (const info of this.indices) { 224 408 if ( ··· 265 449 container.id = "margin-overlay-container"; 266 450 sidebarShadow.appendChild(container); 267 451 268 - createTooltip(container); 269 - 270 452 const observer = new ResizeObserver(() => { 271 453 sidebarHost.style.height = `${getScrollHeight()}px`; 272 454 }); 273 455 if (document.body) observer.observe(document.body); 274 456 if (document.documentElement) observer.observe(document.documentElement); 275 457 276 - fetchAnnotations(); 458 + if (typeof chrome !== "undefined" && chrome.storage) { 459 + chrome.storage.local.get(["showOverlay"], (result) => { 460 + if (result.showOverlay === false) { 461 + sidebarHost.style.display = "none"; 462 + } else { 463 + fetchAnnotations(); 464 + } 465 + }); 466 + } else { 467 + fetchAnnotations(); 468 + } 277 469 278 470 document.addEventListener("mousemove", handleMouseMove); 279 - document.addEventListener("click", handleDocumentClick); 471 + document.addEventListener("click", handleDocumentClick, true); 280 472 } 281 473 282 - function createTooltip(container) { 283 - tooltipEl = document.createElement("div"); 284 - tooltipEl.className = "margin-badge"; 285 - tooltipEl.style.opacity = "0"; 286 - tooltipEl.style.transition = "opacity 0.1s, transform 0.1s"; 287 - tooltipEl.style.pointerEvents = "auto"; 474 + function showInlineComposeModal() { 475 + if (!sidebarShadow || !currentSelection) return; 476 + 477 + const container = sidebarShadow.getElementById("margin-overlay-container"); 478 + if (!container) return; 479 + 480 + const existingModal = container.querySelector(".inline-compose-modal"); 481 + if (existingModal) existingModal.remove(); 482 + 483 + const modal = document.createElement("div"); 484 + modal.className = "inline-compose-modal"; 485 + 486 + modal.style.left = `${Math.max(20, (window.innerWidth - 340) / 2)}px`; 487 + modal.style.top = `${Math.min(200, window.innerHeight / 4)}px`; 488 + 489 + const truncatedQuote = 490 + currentSelection.text.length > 100 491 + ? currentSelection.text.substring(0, 100) + "..." 492 + : currentSelection.text; 493 + 494 + modal.innerHTML = ` 495 + <div class="inline-compose-quote">"${truncatedQuote}"</div> 496 + <textarea class="inline-compose-textarea" placeholder="Add your annotation..." autofocus></textarea> 497 + <div class="inline-compose-actions"> 498 + <button class="btn-cancel">Cancel</button> 499 + <button class="btn-submit">Post Annotation</button> 500 + </div> 501 + `; 502 + 503 + const textarea = modal.querySelector("textarea"); 504 + const submitBtn = modal.querySelector(".btn-submit"); 505 + const cancelBtn = modal.querySelector(".btn-cancel"); 288 506 289 - tooltipEl.addEventListener("click", (e) => { 290 - e.stopPropagation(); 291 - if (hoveredItems.length > 0) { 292 - const firstItem = hoveredItems[0]; 293 - const rect = activeItems 294 - .find((x) => x.item === firstItem) 295 - ?.range.getBoundingClientRect(); 296 - if (rect) { 297 - const top = rect.top + window.scrollY; 298 - const left = rect.left + window.scrollX; 299 - showPopover(hoveredItems, top, left); 300 - } 301 - } 507 + cancelBtn.addEventListener("click", () => { 508 + modal.remove(); 302 509 }); 303 - container.appendChild(tooltipEl); 510 + 511 + submitBtn.addEventListener("click", async () => { 512 + const text = textarea.value.trim(); 513 + if (!text) return; 514 + 515 + submitBtn.disabled = true; 516 + submitBtn.textContent = "Posting..."; 517 + 518 + chrome.runtime.sendMessage( 519 + { 520 + type: "CREATE_ANNOTATION", 521 + data: { 522 + url: currentSelection.url || window.location.href, 523 + title: currentSelection.title || document.title, 524 + text: text, 525 + selector: currentSelection.selector, 526 + }, 527 + }, 528 + (res) => { 529 + if (res && res.success) { 530 + modal.remove(); 531 + fetchAnnotations(); 532 + } else { 533 + submitBtn.disabled = false; 534 + submitBtn.textContent = "Post Annotation"; 535 + alert( 536 + "Failed to create annotation: " + (res?.error || "Unknown error"), 537 + ); 538 + } 539 + }, 540 + ); 541 + }); 542 + 543 + container.appendChild(modal); 544 + textarea.focus(); 545 + 546 + const handleEscape = (e) => { 547 + if (e.key === "Escape") { 548 + modal.remove(); 549 + document.removeEventListener("keydown", handleEscape); 550 + } 551 + }; 552 + document.addEventListener("keydown", handleEscape); 304 553 } 305 554 555 + let hoverIndicator = null; 556 + 306 557 function handleMouseMove(e) { 307 558 const x = e.clientX; 308 559 const y = e.clientY; 309 - 310 560 let foundItems = []; 311 - 561 + let firstRange = null; 312 562 for (const { range, item } of activeItems) { 313 563 const rects = range.getClientRects(); 314 564 for (const rect of rects) { 315 - const padding = 5; 316 565 if ( 317 - x >= rect.left - padding && 318 - x <= rect.right + padding && 319 - y >= rect.top - padding && 320 - y <= rect.bottom + padding 566 + x >= rect.left && 567 + x <= rect.right && 568 + y >= rect.top && 569 + y <= rect.bottom 321 570 ) { 322 - if (!foundItems.includes(item)) { 323 - foundItems.push(item); 571 + if (!firstRange) firstRange = range; 572 + if (!foundItems.some((f) => f.item === item)) { 573 + foundItems.push({ range, item, rect }); 324 574 } 325 575 break; 326 576 } 327 577 } 328 578 } 329 579 330 - let isOverTooltip = false; 331 - if (tooltipEl && tooltipEl.style.opacity === "1") { 332 - const rect = tooltipEl.getBoundingClientRect(); 333 - if ( 334 - x >= rect.left && 335 - x <= rect.right && 336 - y >= rect.top && 337 - y <= rect.bottom 338 - ) { 339 - isOverTooltip = true; 340 - } 341 - } 580 + if (foundItems.length > 0) { 581 + document.body.style.cursor = "pointer"; 342 582 343 - if (foundItems.length > 0 || isOverTooltip) { 344 - if (hideTimer) { 345 - clearTimeout(hideTimer); 346 - hideTimer = null; 347 - } 348 - if (foundItems.length > 0) { 349 - const currentIds = hoveredItems 350 - .map((i) => i.id || i.cid) 351 - .sort() 352 - .join(","); 353 - const newIds = foundItems 354 - .map((i) => i.id || i.cid) 355 - .sort() 356 - .join(","); 357 - 358 - if (currentIds !== newIds) { 359 - hoveredItems = foundItems; 360 - updateTooltip(); 583 + if (!hoverIndicator && sidebarShadow) { 584 + const container = sidebarShadow.getElementById( 585 + "margin-overlay-container", 586 + ); 587 + if (container) { 588 + hoverIndicator = document.createElement("div"); 589 + hoverIndicator.className = "margin-hover-indicator"; 590 + hoverIndicator.style.cssText = ` 591 + position: fixed; 592 + display: flex; 593 + align-items: center; 594 + pointer-events: none; 595 + z-index: 2147483647; 596 + opacity: 0; 597 + transition: opacity 0.15s, transform 0.15s; 598 + transform: scale(0.8); 599 + `; 600 + container.appendChild(hoverIndicator); 361 601 } 362 602 } 363 - } else { 364 - if (!hideTimer && hoveredItems.length > 0) { 365 - hideTimer = setTimeout(() => { 366 - hoveredItems = []; 367 - updateTooltip(); 368 - hideTimer = null; 369 - }, 300); 370 - } 371 - } 372 - } 373 603 374 - function updateTooltip() { 375 - if (!tooltipEl) return; 604 + if (hoverIndicator) { 605 + const authorsMap = new Map(); 606 + foundItems.forEach(({ item }) => { 607 + const author = item.author || item.creator || {}; 608 + const id = author.did || author.handle || "unknown"; 609 + if (!authorsMap.has(id)) { 610 + authorsMap.set(id, author); 611 + } 612 + }); 613 + const uniqueAuthors = Array.from(authorsMap.values()); 376 614 377 - if (hoveredItems.length === 0) { 378 - tooltipEl.style.opacity = "0"; 379 - tooltipEl.style.transform = "translateY(-105%) scale(0.9)"; 380 - tooltipEl.style.pointerEvents = "none"; 381 - return; 382 - } 615 + const maxShow = 3; 616 + const displayAuthors = uniqueAuthors.slice(0, maxShow); 617 + const overflow = uniqueAuthors.length - maxShow; 383 618 384 - tooltipEl.style.pointerEvents = "auto"; 619 + let html = displayAuthors 620 + .map((author, i) => { 621 + const avatar = author.avatar; 622 + const handle = author.handle || "U"; 623 + const marginLeft = i === 0 ? "0" : "-8px"; 385 624 386 - const authorsMap = new Map(); 387 - hoveredItems.forEach((item) => { 388 - const author = item.author || item.creator || {}; 389 - const id = author.did || author.handle; 390 - if (id && !authorsMap.has(id)) { 391 - authorsMap.set(id, author); 392 - } 393 - }); 625 + if (avatar) { 626 + return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`; 627 + } else { 628 + return `<div style="width: 24px; height: 24px; border-radius: 50%; background: #6366f1; color: white; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: ${marginLeft};">${handle[0]?.toUpperCase() || "U"}</div>`; 629 + } 630 + }) 631 + .join(""); 394 632 395 - const uniqueAuthors = Array.from(authorsMap.values()); 396 - let contentHtml = ""; 633 + if (overflow > 0) { 634 + html += `<div style="width: 24px; height: 24px; border-radius: 50%; background: #27272a; color: #a1a1aa; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: -8px;">+${overflow}</div>`; 635 + } 397 636 398 - if (uniqueAuthors.length === 1) { 399 - const author = uniqueAuthors[0] || {}; 400 - const handle = author.handle || "User"; 401 - const avatar = author.avatar; 402 - const count = hoveredItems.length; 637 + hoverIndicator.innerHTML = html; 638 + 639 + const firstRect = firstRange.getClientRects()[0]; 640 + const totalWidth = 641 + Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) * 642 + 18 + 643 + 8; 644 + const leftPos = firstRect.left - totalWidth; 645 + const topPos = firstRect.top + firstRect.height / 2 - 12; 403 646 404 - let avatarHtml = `<div class="margin-badge-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 405 - if (avatar) { 406 - avatarHtml = `<img src="${avatar}" class="margin-badge-avatar">`; 647 + hoverIndicator.style.left = `${leftPos}px`; 648 + hoverIndicator.style.top = `${topPos}px`; 649 + hoverIndicator.style.opacity = "1"; 650 + hoverIndicator.style.transform = "scale(1)"; 407 651 } 408 - 409 - contentHtml = `${avatarHtml}<span>${handle}${count > 1 ? ` (${count})` : ""}</span>`; 410 652 } else { 411 - let stackHtml = `<div class="margin-badge-stack">`; 412 - const displayAuthors = uniqueAuthors.slice(0, 3); 413 - displayAuthors.forEach((author) => { 414 - const handle = author.handle || "U"; 415 - const avatar = author.avatar; 416 - if (avatar) { 417 - stackHtml += `<img src="${avatar}" class="margin-badge-avatar">`; 418 - } else { 419 - stackHtml += `<div class="margin-badge-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 420 - } 421 - }); 422 - stackHtml += `</div>`; 653 + document.body.style.cursor = ""; 654 + if (hoverIndicator) { 655 + hoverIndicator.style.opacity = "0"; 656 + hoverIndicator.style.transform = "scale(0.8)"; 657 + } 658 + } 659 + } 423 660 424 - contentHtml = `${stackHtml}<span>${uniqueAuthors.length} people</span>`; 661 + function handleDocumentClick(e) { 662 + const x = e.clientX; 663 + const y = e.clientY; 664 + if (popoverEl && sidebarShadow) { 665 + const rect = popoverEl.getBoundingClientRect(); 666 + if ( 667 + x >= rect.left && 668 + x <= rect.right && 669 + y >= rect.top && 670 + y <= rect.bottom 671 + ) { 672 + return; 673 + } 425 674 } 426 675 427 - tooltipEl.innerHTML = ` 428 - ${contentHtml} 429 - <div class="margin-badge-stem"></div> 430 - `; 431 - 432 - const firstItem = hoveredItems[0]; 433 - const match = activeItems.find((x) => x.item === firstItem); 434 - if (match) { 435 - const rects = match.range.getClientRects(); 436 - if (rects && rects.length > 0) { 437 - const rect = rects[0]; 438 - const top = rect.top + window.scrollY; 439 - const left = rect.left + window.scrollX; 440 - 441 - tooltipEl.style.top = `${top - 36}px`; 442 - tooltipEl.style.left = `${left}px`; 443 - tooltipEl.style.opacity = "1"; 444 - tooltipEl.style.transform = "translateY(0) scale(1)"; 676 + let clickedItems = []; 677 + for (const { range, item } of activeItems) { 678 + const rects = range.getClientRects(); 679 + for (const rect of rects) { 680 + if ( 681 + x >= rect.left && 682 + x <= rect.right && 683 + y >= rect.top && 684 + y <= rect.bottom 685 + ) { 686 + if (!clickedItems.includes(item)) { 687 + clickedItems.push(item); 688 + } 689 + break; 690 + } 445 691 } 446 692 } 447 - } 448 693 449 - function handleDocumentClick(e) { 450 - if (hoveredItems.length > 0) { 694 + if (clickedItems.length > 0) { 451 695 e.preventDefault(); 452 696 e.stopPropagation(); 453 697 454 - const item = hoveredItems[0]; 455 - const match = activeItems.find((x) => x.item === item); 698 + if (popoverEl) { 699 + const currentIds = popoverEl.dataset.itemIds; 700 + const newIds = clickedItems 701 + .map((i) => i.uri || i.id) 702 + .sort() 703 + .join(","); 704 + 705 + if (currentIds === newIds) { 706 + popoverEl.remove(); 707 + popoverEl = null; 708 + return; 709 + } 710 + } 711 + 712 + const firstItem = clickedItems[0]; 713 + const match = activeItems.find((x) => x.item === firstItem); 456 714 if (match) { 457 715 const rects = match.range.getClientRects(); 458 716 if (rects.length > 0) { 459 717 const rect = rects[0]; 460 718 const top = rect.top + window.scrollY; 461 719 const left = rect.left + window.scrollX; 462 - showPopover(hoveredItems, top, left); 720 + showPopover(clickedItems, top, left); 463 721 } 464 722 } 723 + } else { 724 + if (popoverEl) { 725 + popoverEl.remove(); 726 + popoverEl = null; 727 + } 465 728 } 466 729 } 467 - 468 - function refreshPositions() {} 469 730 470 731 function renderBadges(annotations) { 471 732 if (!sidebarShadow) return; ··· 484 745 if (range) { 485 746 activeItems.push({ range, item }); 486 747 487 - const color = item.color || "#c084fc"; 748 + const color = item.color || "#6366f1"; 488 749 if (!rangesByColor[color]) rangesByColor[color] = []; 489 750 rangesByColor[color].push(range); 490 751 } 491 752 }); 492 753 493 - if (CSS.highlights) { 754 + if (typeof CSS !== "undefined" && CSS.highlights) { 494 755 CSS.highlights.clear(); 495 756 for (const [color, ranges] of Object.entries(rangesByColor)) { 496 757 const highlight = new Highlight(...ranges); ··· 508 769 const style = document.createElement("style"); 509 770 style.textContent = ` 510 771 ::highlight(${name}) { 511 - background-color: ${color}66; 512 - color: inherit; 772 + text-decoration: underline; 773 + text-decoration-color: ${color}; 774 + text-decoration-thickness: 2px; 775 + text-underline-offset: 2px; 513 776 cursor: pointer; 514 777 } 515 778 `; ··· 523 786 popoverEl = document.createElement("div"); 524 787 popoverEl.className = "margin-popover"; 525 788 789 + const ids = items 790 + .map((i) => i.uri || i.id) 791 + .sort() 792 + .join(","); 793 + popoverEl.dataset.itemIds = ids; 794 + 526 795 const popWidth = 320; 527 796 const screenWidth = window.innerWidth; 528 797 let finalLeft = left; ··· 531 800 popoverEl.style.top = `${top + 20}px`; 532 801 popoverEl.style.left = `${finalLeft}px`; 533 802 534 - const title = 535 - items.length > 1 ? `${items.length} Annotations` : "Annotation"; 803 + const hasHighlights = items.some((item) => item.type === "Highlight"); 804 + const hasAnnotations = items.some((item) => item.type !== "Highlight"); 805 + let title; 806 + if (items.length > 1) { 807 + if (hasHighlights && hasAnnotations) { 808 + title = `${items.length} Items`; 809 + } else if (hasHighlights) { 810 + title = `${items.length} Highlights`; 811 + } else { 812 + title = `${items.length} Annotations`; 813 + } 814 + } else { 815 + title = items[0]?.type === "Highlight" ? "Highlight" : "Annotation"; 816 + } 536 817 537 818 let contentHtml = items 538 819 .map((item) => { ··· 591 872 </div> 592 873 `; 593 874 594 - popoverEl.querySelector(".popover-close").addEventListener("click", () => { 875 + popoverEl.querySelector(".popover-close").addEventListener("click", (e) => { 876 + e.stopPropagation(); 595 877 popoverEl.remove(); 596 878 popoverEl = null; 597 879 }); 598 880 599 881 const replyBtns = popoverEl.querySelectorAll(".btn-reply"); 600 882 replyBtns.forEach((btn) => { 601 - btn.addEventListener("click", () => { 883 + btn.addEventListener("click", (e) => { 884 + e.stopPropagation(); 602 885 const id = btn.getAttribute("data-id"); 603 886 if (id) { 604 887 chrome.runtime.sendMessage({ ··· 636 919 }, 0); 637 920 } 638 921 639 - function closePopoverOutside(e) { 922 + function closePopoverOutside() { 640 923 if (popoverEl) { 641 924 popoverEl.remove(); 642 925 popoverEl = null; ··· 644 927 } 645 928 } 646 929 647 - function fetchAnnotations() { 930 + function fetchAnnotations(retryCount = 0) { 648 931 if (typeof chrome !== "undefined" && chrome.runtime) { 649 932 chrome.runtime.sendMessage( 650 933 { ··· 652 935 data: { url: window.location.href }, 653 936 }, 654 937 (res) => { 655 - if (res && res.success) { 938 + if (res && res.success && res.data && res.data.length > 0) { 656 939 renderBadges(res.data); 940 + } else if (retryCount < 3) { 941 + setTimeout( 942 + () => fetchAnnotations(retryCount + 1), 943 + 1000 * (retryCount + 1), 944 + ); 657 945 } 658 946 }, 659 947 ); ··· 672 960 return true; 673 961 } 674 962 963 + if (request.type === "SHOW_INLINE_ANNOTATE") { 964 + currentSelection = { 965 + text: request.data.selector?.exact || "", 966 + selector: request.data.selector, 967 + url: request.data.url, 968 + title: request.data.title, 969 + }; 970 + showInlineComposeModal(); 971 + sendResponse({ success: true }); 972 + return true; 973 + } 974 + 975 + if (request.type === "UPDATE_OVERLAY_VISIBILITY") { 976 + if (sidebarHost) { 977 + sidebarHost.style.display = request.show ? "block" : "none"; 978 + } 979 + if (request.show) { 980 + fetchAnnotations(); 981 + } else { 982 + if (typeof CSS !== "undefined" && CSS.highlights) { 983 + CSS.highlights.clear(); 984 + } 985 + } 986 + sendResponse({ success: true }); 987 + return true; 988 + } 989 + 675 990 if (request.type === "SCROLL_TO_TEXT") { 676 991 const selector = request.selector; 677 992 if (selector?.exact) { ··· 698 1013 } else { 699 1014 initOverlay(); 700 1015 } 1016 + 1017 + window.addEventListener("load", () => { 1018 + if (typeof chrome !== "undefined" && chrome.storage) { 1019 + chrome.storage.local.get(["showOverlay"], (result) => { 1020 + if (result.showOverlay !== false) { 1021 + setTimeout(() => fetchAnnotations(), 500); 1022 + } 1023 + }); 1024 + } else { 1025 + setTimeout(() => fetchAnnotations(), 500); 1026 + } 1027 + }); 1028 + 1029 + let lastUrl = window.location.href; 1030 + 1031 + function checkUrlChange() { 1032 + if (window.location.href !== lastUrl) { 1033 + lastUrl = window.location.href; 1034 + onUrlChange(); 1035 + } 1036 + } 1037 + 1038 + function onUrlChange() { 1039 + if (typeof CSS !== "undefined" && CSS.highlights) { 1040 + CSS.highlights.clear(); 1041 + } 1042 + activeItems = []; 1043 + 1044 + if (typeof chrome !== "undefined" && chrome.storage) { 1045 + chrome.storage.local.get(["showOverlay"], (result) => { 1046 + if (result.showOverlay !== false) { 1047 + fetchAnnotations(); 1048 + } 1049 + }); 1050 + } else { 1051 + fetchAnnotations(); 1052 + } 1053 + } 1054 + 1055 + window.addEventListener("popstate", onUrlChange); 1056 + 1057 + const originalPushState = history.pushState; 1058 + const originalReplaceState = history.replaceState; 1059 + 1060 + history.pushState = function (...args) { 1061 + originalPushState.apply(this, args); 1062 + checkUrlChange(); 1063 + }; 1064 + 1065 + history.replaceState = function (...args) { 1066 + originalReplaceState.apply(this, args); 1067 + checkUrlChange(); 1068 + }; 1069 + 1070 + setInterval(checkUrlChange, 1000); 701 1071 })();
+25
extension/eslint.config.js
··· 1 + import js from "@eslint/js"; 2 + import globals from "globals"; 3 + 4 + export default [ 5 + { ignores: ["dist"] }, 6 + { 7 + files: ["**/*.js"], 8 + languageOptions: { 9 + ecmaVersion: 2020, 10 + globals: { 11 + ...globals.browser, 12 + ...globals.webextensions, 13 + }, 14 + parserOptions: { 15 + ecmaVersion: "latest", 16 + sourceType: "module", 17 + }, 18 + }, 19 + rules: { 20 + ...js.configs.recommended.rules, 21 + "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 22 + "no-undef": "warn", 23 + }, 24 + }, 25 + ];
+1 -1
extension/icons/site.webmanifest
··· 16 16 "theme_color": "#ffffff", 17 17 "background_color": "#ffffff", 18 18 "display": "standalone" 19 - } 19 + }
+1091
extension/package-lock.json
··· 1 + { 2 + "name": "margin-extension", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "margin-extension", 9 + "version": "0.1.0", 10 + "devDependencies": { 11 + "@eslint/js": "^9.39.2", 12 + "eslint": "^9.39.2", 13 + "globals": "^17.0.0" 14 + } 15 + }, 16 + "node_modules/@eslint-community/eslint-utils": { 17 + "version": "4.9.1", 18 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", 19 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", 20 + "dev": true, 21 + "license": "MIT", 22 + "dependencies": { 23 + "eslint-visitor-keys": "^3.4.3" 24 + }, 25 + "engines": { 26 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 27 + }, 28 + "funding": { 29 + "url": "https://opencollective.com/eslint" 30 + }, 31 + "peerDependencies": { 32 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 33 + } 34 + }, 35 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 36 + "version": "3.4.3", 37 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 38 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 39 + "dev": true, 40 + "license": "Apache-2.0", 41 + "engines": { 42 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 43 + }, 44 + "funding": { 45 + "url": "https://opencollective.com/eslint" 46 + } 47 + }, 48 + "node_modules/@eslint-community/regexpp": { 49 + "version": "4.12.2", 50 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 51 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 52 + "dev": true, 53 + "license": "MIT", 54 + "engines": { 55 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 56 + } 57 + }, 58 + "node_modules/@eslint/config-array": { 59 + "version": "0.21.1", 60 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 61 + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 62 + "dev": true, 63 + "license": "Apache-2.0", 64 + "dependencies": { 65 + "@eslint/object-schema": "^2.1.7", 66 + "debug": "^4.3.1", 67 + "minimatch": "^3.1.2" 68 + }, 69 + "engines": { 70 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 71 + } 72 + }, 73 + "node_modules/@eslint/config-helpers": { 74 + "version": "0.4.2", 75 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 76 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 77 + "dev": true, 78 + "license": "Apache-2.0", 79 + "dependencies": { 80 + "@eslint/core": "^0.17.0" 81 + }, 82 + "engines": { 83 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 84 + } 85 + }, 86 + "node_modules/@eslint/core": { 87 + "version": "0.17.0", 88 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 89 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 90 + "dev": true, 91 + "license": "Apache-2.0", 92 + "dependencies": { 93 + "@types/json-schema": "^7.0.15" 94 + }, 95 + "engines": { 96 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 97 + } 98 + }, 99 + "node_modules/@eslint/eslintrc": { 100 + "version": "3.3.3", 101 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", 102 + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", 103 + "dev": true, 104 + "license": "MIT", 105 + "dependencies": { 106 + "ajv": "^6.12.4", 107 + "debug": "^4.3.2", 108 + "espree": "^10.0.1", 109 + "globals": "^14.0.0", 110 + "ignore": "^5.2.0", 111 + "import-fresh": "^3.2.1", 112 + "js-yaml": "^4.1.1", 113 + "minimatch": "^3.1.2", 114 + "strip-json-comments": "^3.1.1" 115 + }, 116 + "engines": { 117 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 118 + }, 119 + "funding": { 120 + "url": "https://opencollective.com/eslint" 121 + } 122 + }, 123 + "node_modules/@eslint/eslintrc/node_modules/globals": { 124 + "version": "14.0.0", 125 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 126 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 127 + "dev": true, 128 + "license": "MIT", 129 + "engines": { 130 + "node": ">=18" 131 + }, 132 + "funding": { 133 + "url": "https://github.com/sponsors/sindresorhus" 134 + } 135 + }, 136 + "node_modules/@eslint/js": { 137 + "version": "9.39.2", 138 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", 139 + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", 140 + "dev": true, 141 + "license": "MIT", 142 + "engines": { 143 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 144 + }, 145 + "funding": { 146 + "url": "https://eslint.org/donate" 147 + } 148 + }, 149 + "node_modules/@eslint/object-schema": { 150 + "version": "2.1.7", 151 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 152 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 153 + "dev": true, 154 + "license": "Apache-2.0", 155 + "engines": { 156 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 157 + } 158 + }, 159 + "node_modules/@eslint/plugin-kit": { 160 + "version": "0.4.1", 161 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 162 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 163 + "dev": true, 164 + "license": "Apache-2.0", 165 + "dependencies": { 166 + "@eslint/core": "^0.17.0", 167 + "levn": "^0.4.1" 168 + }, 169 + "engines": { 170 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 171 + } 172 + }, 173 + "node_modules/@humanfs/core": { 174 + "version": "0.19.1", 175 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 176 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 177 + "dev": true, 178 + "license": "Apache-2.0", 179 + "engines": { 180 + "node": ">=18.18.0" 181 + } 182 + }, 183 + "node_modules/@humanfs/node": { 184 + "version": "0.16.7", 185 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 186 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 187 + "dev": true, 188 + "license": "Apache-2.0", 189 + "dependencies": { 190 + "@humanfs/core": "^0.19.1", 191 + "@humanwhocodes/retry": "^0.4.0" 192 + }, 193 + "engines": { 194 + "node": ">=18.18.0" 195 + } 196 + }, 197 + "node_modules/@humanwhocodes/module-importer": { 198 + "version": "1.0.1", 199 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 200 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 201 + "dev": true, 202 + "license": "Apache-2.0", 203 + "engines": { 204 + "node": ">=12.22" 205 + }, 206 + "funding": { 207 + "type": "github", 208 + "url": "https://github.com/sponsors/nzakas" 209 + } 210 + }, 211 + "node_modules/@humanwhocodes/retry": { 212 + "version": "0.4.3", 213 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 214 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 215 + "dev": true, 216 + "license": "Apache-2.0", 217 + "engines": { 218 + "node": ">=18.18" 219 + }, 220 + "funding": { 221 + "type": "github", 222 + "url": "https://github.com/sponsors/nzakas" 223 + } 224 + }, 225 + "node_modules/@types/estree": { 226 + "version": "1.0.8", 227 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 228 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 229 + "dev": true, 230 + "license": "MIT" 231 + }, 232 + "node_modules/@types/json-schema": { 233 + "version": "7.0.15", 234 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 235 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 236 + "dev": true, 237 + "license": "MIT" 238 + }, 239 + "node_modules/acorn": { 240 + "version": "8.15.0", 241 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 242 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 243 + "dev": true, 244 + "license": "MIT", 245 + "peer": true, 246 + "bin": { 247 + "acorn": "bin/acorn" 248 + }, 249 + "engines": { 250 + "node": ">=0.4.0" 251 + } 252 + }, 253 + "node_modules/acorn-jsx": { 254 + "version": "5.3.2", 255 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 256 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 257 + "dev": true, 258 + "license": "MIT", 259 + "peerDependencies": { 260 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 261 + } 262 + }, 263 + "node_modules/ajv": { 264 + "version": "6.12.6", 265 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 266 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 267 + "dev": true, 268 + "license": "MIT", 269 + "dependencies": { 270 + "fast-deep-equal": "^3.1.1", 271 + "fast-json-stable-stringify": "^2.0.0", 272 + "json-schema-traverse": "^0.4.1", 273 + "uri-js": "^4.2.2" 274 + }, 275 + "funding": { 276 + "type": "github", 277 + "url": "https://github.com/sponsors/epoberezkin" 278 + } 279 + }, 280 + "node_modules/ansi-styles": { 281 + "version": "4.3.0", 282 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 283 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 284 + "dev": true, 285 + "license": "MIT", 286 + "dependencies": { 287 + "color-convert": "^2.0.1" 288 + }, 289 + "engines": { 290 + "node": ">=8" 291 + }, 292 + "funding": { 293 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 294 + } 295 + }, 296 + "node_modules/argparse": { 297 + "version": "2.0.1", 298 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 299 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 300 + "dev": true, 301 + "license": "Python-2.0" 302 + }, 303 + "node_modules/balanced-match": { 304 + "version": "1.0.2", 305 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 306 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 307 + "dev": true, 308 + "license": "MIT" 309 + }, 310 + "node_modules/brace-expansion": { 311 + "version": "1.1.12", 312 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 313 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 314 + "dev": true, 315 + "license": "MIT", 316 + "dependencies": { 317 + "balanced-match": "^1.0.0", 318 + "concat-map": "0.0.1" 319 + } 320 + }, 321 + "node_modules/callsites": { 322 + "version": "3.1.0", 323 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 324 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 325 + "dev": true, 326 + "license": "MIT", 327 + "engines": { 328 + "node": ">=6" 329 + } 330 + }, 331 + "node_modules/chalk": { 332 + "version": "4.1.2", 333 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 334 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 335 + "dev": true, 336 + "license": "MIT", 337 + "dependencies": { 338 + "ansi-styles": "^4.1.0", 339 + "supports-color": "^7.1.0" 340 + }, 341 + "engines": { 342 + "node": ">=10" 343 + }, 344 + "funding": { 345 + "url": "https://github.com/chalk/chalk?sponsor=1" 346 + } 347 + }, 348 + "node_modules/color-convert": { 349 + "version": "2.0.1", 350 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 351 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 352 + "dev": true, 353 + "license": "MIT", 354 + "dependencies": { 355 + "color-name": "~1.1.4" 356 + }, 357 + "engines": { 358 + "node": ">=7.0.0" 359 + } 360 + }, 361 + "node_modules/color-name": { 362 + "version": "1.1.4", 363 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 364 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 365 + "dev": true, 366 + "license": "MIT" 367 + }, 368 + "node_modules/concat-map": { 369 + "version": "0.0.1", 370 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 371 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 372 + "dev": true, 373 + "license": "MIT" 374 + }, 375 + "node_modules/cross-spawn": { 376 + "version": "7.0.6", 377 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 378 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 379 + "dev": true, 380 + "license": "MIT", 381 + "dependencies": { 382 + "path-key": "^3.1.0", 383 + "shebang-command": "^2.0.0", 384 + "which": "^2.0.1" 385 + }, 386 + "engines": { 387 + "node": ">= 8" 388 + } 389 + }, 390 + "node_modules/debug": { 391 + "version": "4.4.3", 392 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 393 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 394 + "dev": true, 395 + "license": "MIT", 396 + "dependencies": { 397 + "ms": "^2.1.3" 398 + }, 399 + "engines": { 400 + "node": ">=6.0" 401 + }, 402 + "peerDependenciesMeta": { 403 + "supports-color": { 404 + "optional": true 405 + } 406 + } 407 + }, 408 + "node_modules/deep-is": { 409 + "version": "0.1.4", 410 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 411 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 412 + "dev": true, 413 + "license": "MIT" 414 + }, 415 + "node_modules/escape-string-regexp": { 416 + "version": "4.0.0", 417 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 418 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 419 + "dev": true, 420 + "license": "MIT", 421 + "engines": { 422 + "node": ">=10" 423 + }, 424 + "funding": { 425 + "url": "https://github.com/sponsors/sindresorhus" 426 + } 427 + }, 428 + "node_modules/eslint": { 429 + "version": "9.39.2", 430 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", 431 + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 432 + "dev": true, 433 + "license": "MIT", 434 + "peer": true, 435 + "dependencies": { 436 + "@eslint-community/eslint-utils": "^4.8.0", 437 + "@eslint-community/regexpp": "^4.12.1", 438 + "@eslint/config-array": "^0.21.1", 439 + "@eslint/config-helpers": "^0.4.2", 440 + "@eslint/core": "^0.17.0", 441 + "@eslint/eslintrc": "^3.3.1", 442 + "@eslint/js": "9.39.2", 443 + "@eslint/plugin-kit": "^0.4.1", 444 + "@humanfs/node": "^0.16.6", 445 + "@humanwhocodes/module-importer": "^1.0.1", 446 + "@humanwhocodes/retry": "^0.4.2", 447 + "@types/estree": "^1.0.6", 448 + "ajv": "^6.12.4", 449 + "chalk": "^4.0.0", 450 + "cross-spawn": "^7.0.6", 451 + "debug": "^4.3.2", 452 + "escape-string-regexp": "^4.0.0", 453 + "eslint-scope": "^8.4.0", 454 + "eslint-visitor-keys": "^4.2.1", 455 + "espree": "^10.4.0", 456 + "esquery": "^1.5.0", 457 + "esutils": "^2.0.2", 458 + "fast-deep-equal": "^3.1.3", 459 + "file-entry-cache": "^8.0.0", 460 + "find-up": "^5.0.0", 461 + "glob-parent": "^6.0.2", 462 + "ignore": "^5.2.0", 463 + "imurmurhash": "^0.1.4", 464 + "is-glob": "^4.0.0", 465 + "json-stable-stringify-without-jsonify": "^1.0.1", 466 + "lodash.merge": "^4.6.2", 467 + "minimatch": "^3.1.2", 468 + "natural-compare": "^1.4.0", 469 + "optionator": "^0.9.3" 470 + }, 471 + "bin": { 472 + "eslint": "bin/eslint.js" 473 + }, 474 + "engines": { 475 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 476 + }, 477 + "funding": { 478 + "url": "https://eslint.org/donate" 479 + }, 480 + "peerDependencies": { 481 + "jiti": "*" 482 + }, 483 + "peerDependenciesMeta": { 484 + "jiti": { 485 + "optional": true 486 + } 487 + } 488 + }, 489 + "node_modules/eslint-scope": { 490 + "version": "8.4.0", 491 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 492 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 493 + "dev": true, 494 + "license": "BSD-2-Clause", 495 + "dependencies": { 496 + "esrecurse": "^4.3.0", 497 + "estraverse": "^5.2.0" 498 + }, 499 + "engines": { 500 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 501 + }, 502 + "funding": { 503 + "url": "https://opencollective.com/eslint" 504 + } 505 + }, 506 + "node_modules/eslint-visitor-keys": { 507 + "version": "4.2.1", 508 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 509 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 510 + "dev": true, 511 + "license": "Apache-2.0", 512 + "engines": { 513 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 514 + }, 515 + "funding": { 516 + "url": "https://opencollective.com/eslint" 517 + } 518 + }, 519 + "node_modules/espree": { 520 + "version": "10.4.0", 521 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 522 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 523 + "dev": true, 524 + "license": "BSD-2-Clause", 525 + "dependencies": { 526 + "acorn": "^8.15.0", 527 + "acorn-jsx": "^5.3.2", 528 + "eslint-visitor-keys": "^4.2.1" 529 + }, 530 + "engines": { 531 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 532 + }, 533 + "funding": { 534 + "url": "https://opencollective.com/eslint" 535 + } 536 + }, 537 + "node_modules/esquery": { 538 + "version": "1.7.0", 539 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", 540 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", 541 + "dev": true, 542 + "license": "BSD-3-Clause", 543 + "dependencies": { 544 + "estraverse": "^5.1.0" 545 + }, 546 + "engines": { 547 + "node": ">=0.10" 548 + } 549 + }, 550 + "node_modules/esrecurse": { 551 + "version": "4.3.0", 552 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 553 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 554 + "dev": true, 555 + "license": "BSD-2-Clause", 556 + "dependencies": { 557 + "estraverse": "^5.2.0" 558 + }, 559 + "engines": { 560 + "node": ">=4.0" 561 + } 562 + }, 563 + "node_modules/estraverse": { 564 + "version": "5.3.0", 565 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 566 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 567 + "dev": true, 568 + "license": "BSD-2-Clause", 569 + "engines": { 570 + "node": ">=4.0" 571 + } 572 + }, 573 + "node_modules/esutils": { 574 + "version": "2.0.3", 575 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 576 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 577 + "dev": true, 578 + "license": "BSD-2-Clause", 579 + "engines": { 580 + "node": ">=0.10.0" 581 + } 582 + }, 583 + "node_modules/fast-deep-equal": { 584 + "version": "3.1.3", 585 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 586 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 587 + "dev": true, 588 + "license": "MIT" 589 + }, 590 + "node_modules/fast-json-stable-stringify": { 591 + "version": "2.1.0", 592 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 593 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 594 + "dev": true, 595 + "license": "MIT" 596 + }, 597 + "node_modules/fast-levenshtein": { 598 + "version": "2.0.6", 599 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 600 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 601 + "dev": true, 602 + "license": "MIT" 603 + }, 604 + "node_modules/file-entry-cache": { 605 + "version": "8.0.0", 606 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 607 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 608 + "dev": true, 609 + "license": "MIT", 610 + "dependencies": { 611 + "flat-cache": "^4.0.0" 612 + }, 613 + "engines": { 614 + "node": ">=16.0.0" 615 + } 616 + }, 617 + "node_modules/find-up": { 618 + "version": "5.0.0", 619 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 620 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 621 + "dev": true, 622 + "license": "MIT", 623 + "dependencies": { 624 + "locate-path": "^6.0.0", 625 + "path-exists": "^4.0.0" 626 + }, 627 + "engines": { 628 + "node": ">=10" 629 + }, 630 + "funding": { 631 + "url": "https://github.com/sponsors/sindresorhus" 632 + } 633 + }, 634 + "node_modules/flat-cache": { 635 + "version": "4.0.1", 636 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 637 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 638 + "dev": true, 639 + "license": "MIT", 640 + "dependencies": { 641 + "flatted": "^3.2.9", 642 + "keyv": "^4.5.4" 643 + }, 644 + "engines": { 645 + "node": ">=16" 646 + } 647 + }, 648 + "node_modules/flatted": { 649 + "version": "3.3.3", 650 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 651 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 652 + "dev": true, 653 + "license": "ISC" 654 + }, 655 + "node_modules/glob-parent": { 656 + "version": "6.0.2", 657 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 658 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 659 + "dev": true, 660 + "license": "ISC", 661 + "dependencies": { 662 + "is-glob": "^4.0.3" 663 + }, 664 + "engines": { 665 + "node": ">=10.13.0" 666 + } 667 + }, 668 + "node_modules/globals": { 669 + "version": "17.0.0", 670 + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", 671 + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", 672 + "dev": true, 673 + "license": "MIT", 674 + "engines": { 675 + "node": ">=18" 676 + }, 677 + "funding": { 678 + "url": "https://github.com/sponsors/sindresorhus" 679 + } 680 + }, 681 + "node_modules/has-flag": { 682 + "version": "4.0.0", 683 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 684 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 685 + "dev": true, 686 + "license": "MIT", 687 + "engines": { 688 + "node": ">=8" 689 + } 690 + }, 691 + "node_modules/ignore": { 692 + "version": "5.3.2", 693 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 694 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 695 + "dev": true, 696 + "license": "MIT", 697 + "engines": { 698 + "node": ">= 4" 699 + } 700 + }, 701 + "node_modules/import-fresh": { 702 + "version": "3.3.1", 703 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 704 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 705 + "dev": true, 706 + "license": "MIT", 707 + "dependencies": { 708 + "parent-module": "^1.0.0", 709 + "resolve-from": "^4.0.0" 710 + }, 711 + "engines": { 712 + "node": ">=6" 713 + }, 714 + "funding": { 715 + "url": "https://github.com/sponsors/sindresorhus" 716 + } 717 + }, 718 + "node_modules/imurmurhash": { 719 + "version": "0.1.4", 720 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 721 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 722 + "dev": true, 723 + "license": "MIT", 724 + "engines": { 725 + "node": ">=0.8.19" 726 + } 727 + }, 728 + "node_modules/is-extglob": { 729 + "version": "2.1.1", 730 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 731 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 732 + "dev": true, 733 + "license": "MIT", 734 + "engines": { 735 + "node": ">=0.10.0" 736 + } 737 + }, 738 + "node_modules/is-glob": { 739 + "version": "4.0.3", 740 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 741 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 742 + "dev": true, 743 + "license": "MIT", 744 + "dependencies": { 745 + "is-extglob": "^2.1.1" 746 + }, 747 + "engines": { 748 + "node": ">=0.10.0" 749 + } 750 + }, 751 + "node_modules/isexe": { 752 + "version": "2.0.0", 753 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 754 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 755 + "dev": true, 756 + "license": "ISC" 757 + }, 758 + "node_modules/js-yaml": { 759 + "version": "4.1.1", 760 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 761 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 762 + "dev": true, 763 + "license": "MIT", 764 + "dependencies": { 765 + "argparse": "^2.0.1" 766 + }, 767 + "bin": { 768 + "js-yaml": "bin/js-yaml.js" 769 + } 770 + }, 771 + "node_modules/json-buffer": { 772 + "version": "3.0.1", 773 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 774 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 775 + "dev": true, 776 + "license": "MIT" 777 + }, 778 + "node_modules/json-schema-traverse": { 779 + "version": "0.4.1", 780 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 781 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 782 + "dev": true, 783 + "license": "MIT" 784 + }, 785 + "node_modules/json-stable-stringify-without-jsonify": { 786 + "version": "1.0.1", 787 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 788 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 789 + "dev": true, 790 + "license": "MIT" 791 + }, 792 + "node_modules/keyv": { 793 + "version": "4.5.4", 794 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 795 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 796 + "dev": true, 797 + "license": "MIT", 798 + "dependencies": { 799 + "json-buffer": "3.0.1" 800 + } 801 + }, 802 + "node_modules/levn": { 803 + "version": "0.4.1", 804 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 805 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 806 + "dev": true, 807 + "license": "MIT", 808 + "dependencies": { 809 + "prelude-ls": "^1.2.1", 810 + "type-check": "~0.4.0" 811 + }, 812 + "engines": { 813 + "node": ">= 0.8.0" 814 + } 815 + }, 816 + "node_modules/locate-path": { 817 + "version": "6.0.0", 818 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 819 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 820 + "dev": true, 821 + "license": "MIT", 822 + "dependencies": { 823 + "p-locate": "^5.0.0" 824 + }, 825 + "engines": { 826 + "node": ">=10" 827 + }, 828 + "funding": { 829 + "url": "https://github.com/sponsors/sindresorhus" 830 + } 831 + }, 832 + "node_modules/lodash.merge": { 833 + "version": "4.6.2", 834 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 835 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 836 + "dev": true, 837 + "license": "MIT" 838 + }, 839 + "node_modules/minimatch": { 840 + "version": "3.1.2", 841 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 842 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 843 + "dev": true, 844 + "license": "ISC", 845 + "dependencies": { 846 + "brace-expansion": "^1.1.7" 847 + }, 848 + "engines": { 849 + "node": "*" 850 + } 851 + }, 852 + "node_modules/ms": { 853 + "version": "2.1.3", 854 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 855 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 856 + "dev": true, 857 + "license": "MIT" 858 + }, 859 + "node_modules/natural-compare": { 860 + "version": "1.4.0", 861 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 862 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 863 + "dev": true, 864 + "license": "MIT" 865 + }, 866 + "node_modules/optionator": { 867 + "version": "0.9.4", 868 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 869 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 870 + "dev": true, 871 + "license": "MIT", 872 + "dependencies": { 873 + "deep-is": "^0.1.3", 874 + "fast-levenshtein": "^2.0.6", 875 + "levn": "^0.4.1", 876 + "prelude-ls": "^1.2.1", 877 + "type-check": "^0.4.0", 878 + "word-wrap": "^1.2.5" 879 + }, 880 + "engines": { 881 + "node": ">= 0.8.0" 882 + } 883 + }, 884 + "node_modules/p-limit": { 885 + "version": "3.1.0", 886 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 887 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 888 + "dev": true, 889 + "license": "MIT", 890 + "dependencies": { 891 + "yocto-queue": "^0.1.0" 892 + }, 893 + "engines": { 894 + "node": ">=10" 895 + }, 896 + "funding": { 897 + "url": "https://github.com/sponsors/sindresorhus" 898 + } 899 + }, 900 + "node_modules/p-locate": { 901 + "version": "5.0.0", 902 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 903 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 904 + "dev": true, 905 + "license": "MIT", 906 + "dependencies": { 907 + "p-limit": "^3.0.2" 908 + }, 909 + "engines": { 910 + "node": ">=10" 911 + }, 912 + "funding": { 913 + "url": "https://github.com/sponsors/sindresorhus" 914 + } 915 + }, 916 + "node_modules/parent-module": { 917 + "version": "1.0.1", 918 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 919 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 920 + "dev": true, 921 + "license": "MIT", 922 + "dependencies": { 923 + "callsites": "^3.0.0" 924 + }, 925 + "engines": { 926 + "node": ">=6" 927 + } 928 + }, 929 + "node_modules/path-exists": { 930 + "version": "4.0.0", 931 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 932 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 933 + "dev": true, 934 + "license": "MIT", 935 + "engines": { 936 + "node": ">=8" 937 + } 938 + }, 939 + "node_modules/path-key": { 940 + "version": "3.1.1", 941 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 942 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 943 + "dev": true, 944 + "license": "MIT", 945 + "engines": { 946 + "node": ">=8" 947 + } 948 + }, 949 + "node_modules/prelude-ls": { 950 + "version": "1.2.1", 951 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 952 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 953 + "dev": true, 954 + "license": "MIT", 955 + "engines": { 956 + "node": ">= 0.8.0" 957 + } 958 + }, 959 + "node_modules/punycode": { 960 + "version": "2.3.1", 961 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 962 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 963 + "dev": true, 964 + "license": "MIT", 965 + "engines": { 966 + "node": ">=6" 967 + } 968 + }, 969 + "node_modules/resolve-from": { 970 + "version": "4.0.0", 971 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 972 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 973 + "dev": true, 974 + "license": "MIT", 975 + "engines": { 976 + "node": ">=4" 977 + } 978 + }, 979 + "node_modules/shebang-command": { 980 + "version": "2.0.0", 981 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 982 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 983 + "dev": true, 984 + "license": "MIT", 985 + "dependencies": { 986 + "shebang-regex": "^3.0.0" 987 + }, 988 + "engines": { 989 + "node": ">=8" 990 + } 991 + }, 992 + "node_modules/shebang-regex": { 993 + "version": "3.0.0", 994 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 995 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 996 + "dev": true, 997 + "license": "MIT", 998 + "engines": { 999 + "node": ">=8" 1000 + } 1001 + }, 1002 + "node_modules/strip-json-comments": { 1003 + "version": "3.1.1", 1004 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1005 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1006 + "dev": true, 1007 + "license": "MIT", 1008 + "engines": { 1009 + "node": ">=8" 1010 + }, 1011 + "funding": { 1012 + "url": "https://github.com/sponsors/sindresorhus" 1013 + } 1014 + }, 1015 + "node_modules/supports-color": { 1016 + "version": "7.2.0", 1017 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1018 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1019 + "dev": true, 1020 + "license": "MIT", 1021 + "dependencies": { 1022 + "has-flag": "^4.0.0" 1023 + }, 1024 + "engines": { 1025 + "node": ">=8" 1026 + } 1027 + }, 1028 + "node_modules/type-check": { 1029 + "version": "0.4.0", 1030 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1031 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1032 + "dev": true, 1033 + "license": "MIT", 1034 + "dependencies": { 1035 + "prelude-ls": "^1.2.1" 1036 + }, 1037 + "engines": { 1038 + "node": ">= 0.8.0" 1039 + } 1040 + }, 1041 + "node_modules/uri-js": { 1042 + "version": "4.4.1", 1043 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1044 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1045 + "dev": true, 1046 + "license": "BSD-2-Clause", 1047 + "dependencies": { 1048 + "punycode": "^2.1.0" 1049 + } 1050 + }, 1051 + "node_modules/which": { 1052 + "version": "2.0.2", 1053 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1054 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1055 + "dev": true, 1056 + "license": "ISC", 1057 + "dependencies": { 1058 + "isexe": "^2.0.0" 1059 + }, 1060 + "bin": { 1061 + "node-which": "bin/node-which" 1062 + }, 1063 + "engines": { 1064 + "node": ">= 8" 1065 + } 1066 + }, 1067 + "node_modules/word-wrap": { 1068 + "version": "1.2.5", 1069 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 1070 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 1071 + "dev": true, 1072 + "license": "MIT", 1073 + "engines": { 1074 + "node": ">=0.10.0" 1075 + } 1076 + }, 1077 + "node_modules/yocto-queue": { 1078 + "version": "0.1.0", 1079 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1080 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "engines": { 1084 + "node": ">=10" 1085 + }, 1086 + "funding": { 1087 + "url": "https://github.com/sponsors/sindresorhus" 1088 + } 1089 + } 1090 + } 1091 + }
+14
extension/package.json
··· 1 + { 2 + "name": "margin-extension", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "lint": "eslint ." 8 + }, 9 + "devDependencies": { 10 + "@eslint/js": "^9.39.2", 11 + "eslint": "^9.39.2", 12 + "globals": "^17.0.0" 13 + } 14 + }
+62
extension/popup/popup.css
··· 648 648 gap: 8px; 649 649 margin-left: auto; 650 650 } 651 + 652 + .toggle-switch { 653 + position: relative; 654 + display: inline-block; 655 + width: 44px; 656 + height: 24px; 657 + flex-shrink: 0; 658 + } 659 + 660 + .toggle-switch input { 661 + opacity: 0; 662 + width: 0; 663 + height: 0; 664 + } 665 + 666 + .toggle-slider { 667 + position: absolute; 668 + cursor: pointer; 669 + top: 0; 670 + left: 0; 671 + right: 0; 672 + bottom: 0; 673 + background-color: var(--border); 674 + transition: 0.2s; 675 + border-radius: 24px; 676 + } 677 + 678 + .toggle-slider:before { 679 + position: absolute; 680 + content: ""; 681 + height: 18px; 682 + width: 18px; 683 + left: 3px; 684 + bottom: 3px; 685 + background-color: var(--text-secondary); 686 + transition: 0.2s; 687 + border-radius: 50%; 688 + } 689 + 690 + .toggle-switch input:checked + .toggle-slider { 691 + background-color: var(--accent); 692 + } 693 + 694 + .toggle-switch input:checked + .toggle-slider:before { 695 + transform: translateX(20px); 696 + background-color: white; 697 + } 698 + 699 + .settings-input { 700 + width: 100%; 701 + padding: 10px 12px; 702 + background: var(--bg-tertiary); 703 + border: 1px solid var(--border); 704 + border-radius: var(--radius-md); 705 + color: var(--text-primary); 706 + font-size: 13px; 707 + } 708 + 709 + .settings-input:focus { 710 + outline: none; 711 + border-color: var(--accent); 712 + }
+20
extension/popup/popup.html
··· 218 218 <button id="close-settings" class="btn-icon">ร—</button> 219 219 </div> 220 220 <div class="setting-item"> 221 + <div 222 + style=" 223 + display: flex; 224 + justify-content: space-between; 225 + align-items: center; 226 + " 227 + > 228 + <div> 229 + <label>Show page overlays</label> 230 + <p class="setting-help" style="margin-top: 2px"> 231 + Highlights, badges, and tooltips on pages 232 + </p> 233 + </div> 234 + <label class="toggle-switch"> 235 + <input type="checkbox" id="overlay-toggle" checked /> 236 + <span class="toggle-slider"></span> 237 + </label> 238 + </div> 239 + </div> 240 + <div class="setting-item"> 221 241 <label for="api-url">API URL (for self-hosting)</label> 222 242 <input 223 243 type="url"
+33 -17
extension/popup/popup.js
··· 39 39 collectionList: document.getElementById("collection-list"), 40 40 collectionLoading: document.getElementById("collection-loading"), 41 41 collectionsEmpty: document.getElementById("collections-empty"), 42 + overlayToggle: document.getElementById("overlay-toggle"), 42 43 }; 43 44 44 45 let currentTab = null; 45 46 let apiUrl = "https://margin.at"; 46 47 let currentUserDid = null; 47 48 let pendingSelector = null; 48 - let activeAnnotationUriForCollection = null; 49 + // let _activeAnnotationUriForCollection = null; 49 50 50 - const storage = await browserAPI.storage.local.get(["apiUrl"]); 51 + const storage = await browserAPI.storage.local.get(["apiUrl", "showOverlay"]); 51 52 if (storage.apiUrl) { 52 53 apiUrl = storage.apiUrl; 53 54 } 54 55 els.apiUrlInput.value = apiUrl; 56 + 57 + if (els.overlayToggle) { 58 + els.overlayToggle.checked = storage.showOverlay !== false; 59 + } 55 60 56 61 try { 57 62 const [tab] = await browserAPI.tabs.query({ ··· 74 79 pendingData = sessionData.pendingAnnotation; 75 80 await browserAPI.storage.session.remove(["pendingAnnotation"]); 76 81 } 77 - } catch (e) {} 82 + } catch { 83 + /* ignore */ 84 + } 78 85 } 79 86 80 87 if (!pendingData) { ··· 209 216 210 217 els.saveSettings?.addEventListener("click", async () => { 211 218 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 219 + const showOverlay = els.overlayToggle?.checked ?? true; 220 + 221 + await browserAPI.storage.local.set({ apiUrl: newUrl, showOverlay }); 212 222 if (newUrl) { 213 - await browserAPI.storage.local.set({ apiUrl: newUrl }); 214 223 apiUrl = newUrl; 215 - await sendMessage({ type: "UPDATE_SETTINGS" }); 216 - views.settings.style.display = "none"; 217 - checkSession(); 218 224 } 225 + await sendMessage({ type: "UPDATE_SETTINGS" }); 226 + 227 + const tabs = await browserAPI.tabs.query({}); 228 + for (const tab of tabs) { 229 + if (tab.id) { 230 + try { 231 + await browserAPI.tabs.sendMessage(tab.id, { 232 + type: "UPDATE_OVERLAY_VISIBILITY", 233 + show: showOverlay, 234 + }); 235 + } catch { 236 + /* ignore */ 237 + } 238 + } 239 + } 240 + 241 + views.settings.style.display = "none"; 242 + checkSession(); 219 243 }); 220 244 221 245 els.closeCollectionSelector?.addEventListener("click", () => { 222 246 views.collectionSelector.style.display = "none"; 223 - activeAnnotationUriForCollection = null; 224 247 }); 225 248 226 249 async function openCollectionSelector(annotationUri) { ··· 228 251 console.error("No currentUserDid, returning early"); 229 252 return; 230 253 } 231 - activeAnnotationUriForCollection = annotationUri; 232 254 views.collectionSelector.style.display = "flex"; 233 255 els.collectionList.innerHTML = ""; 234 256 els.collectionLoading.style.display = "block"; ··· 507 529 const actions = document.createElement("div"); 508 530 actions.className = "annotation-item-actions"; 509 531 510 - if ( 511 - item.author?.did === currentUserDid || 512 - item.creator?.did === currentUserDid 513 - ) { 532 + if (currentUserDid) { 514 533 const folderBtn = document.createElement("button"); 515 534 folderBtn.className = "btn-icon"; 516 535 folderBtn.innerHTML = ··· 580 599 581 600 row.appendChild(content); 582 601 583 - if ( 584 - item.author?.did === currentUserDid || 585 - item.creator?.did === currentUserDid 586 - ) { 602 + if (currentUserDid) { 587 603 const folderBtn = document.createElement("button"); 588 604 folderBtn.className = "btn-icon"; 589 605 folderBtn.innerHTML =
+47
extension/sidepanel/sidepanel.css
··· 882 882 gap: 8px; 883 883 margin-left: auto; 884 884 } 885 + 886 + .toggle-switch { 887 + position: relative; 888 + display: inline-block; 889 + width: 44px; 890 + height: 24px; 891 + flex-shrink: 0; 892 + } 893 + 894 + .toggle-switch input { 895 + opacity: 0; 896 + width: 0; 897 + height: 0; 898 + } 899 + 900 + .toggle-slider { 901 + position: absolute; 902 + cursor: pointer; 903 + top: 0; 904 + left: 0; 905 + right: 0; 906 + bottom: 0; 907 + background-color: var(--border); 908 + transition: 0.2s; 909 + border-radius: 24px; 910 + } 911 + 912 + .toggle-slider:before { 913 + position: absolute; 914 + content: ""; 915 + height: 18px; 916 + width: 18px; 917 + left: 3px; 918 + bottom: 3px; 919 + background-color: var(--text-secondary); 920 + transition: 0.2s; 921 + border-radius: 50%; 922 + } 923 + 924 + .toggle-switch input:checked + .toggle-slider { 925 + background-color: var(--accent); 926 + } 927 + 928 + .toggle-switch input:checked + .toggle-slider:before { 929 + transform: translateX(20px); 930 + background-color: white; 931 + }
+20
extension/sidepanel/sidepanel.html
··· 250 250 <button id="close-settings" class="btn-icon">ร—</button> 251 251 </div> 252 252 <div class="setting-item"> 253 + <div 254 + style=" 255 + display: flex; 256 + justify-content: space-between; 257 + align-items: center; 258 + " 259 + > 260 + <div> 261 + <label>Show page overlays</label> 262 + <p class="setting-help" style="margin-top: 2px"> 263 + Display highlights, badges, and tooltips on pages 264 + </p> 265 + </div> 266 + <label class="toggle-switch"> 267 + <input type="checkbox" id="overlay-toggle" checked /> 268 + <span class="toggle-slider"></span> 269 + </label> 270 + </div> 271 + </div> 272 + <div class="setting-item"> 253 273 <label for="api-url">API URL</label> 254 274 <input 255 275 type="url"
+36 -21
extension/sidepanel/sidepanel.js
··· 37 37 collectionList: document.getElementById("collection-list"), 38 38 collectionLoading: document.getElementById("collection-loading"), 39 39 collectionsEmpty: document.getElementById("collections-empty"), 40 + overlayToggle: document.getElementById("overlay-toggle"), 40 41 }; 41 42 42 43 let currentTab = null; 43 44 let apiUrl = ""; 44 45 let currentUserDid = null; 45 46 let pendingSelector = null; 46 - let activeAnnotationUriForCollection = null; 47 47 48 48 const storage = await chrome.storage.local.get(["apiUrl"]); 49 49 if (storage.apiUrl) { ··· 51 51 } 52 52 53 53 els.apiUrlInput.value = apiUrl; 54 + 55 + const overlayStorage = await chrome.storage.local.get(["showOverlay"]); 56 + if (els.overlayToggle) { 57 + els.overlayToggle.checked = overlayStorage.showOverlay !== false; 58 + } 54 59 55 60 chrome.storage.onChanged.addListener((changes, area) => { 56 61 if (area === "local" && changes.apiUrl) { ··· 253 258 254 259 els.closeCollectionSelector?.addEventListener("click", () => { 255 260 views.collectionSelector.style.display = "none"; 256 - activeAnnotationUriForCollection = null; 257 261 }); 258 262 259 263 els.saveSettings?.addEventListener("click", async () => { 260 264 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 265 + const showOverlay = els.overlayToggle?.checked ?? true; 266 + 267 + await chrome.storage.local.set({ apiUrl: newUrl, showOverlay }); 261 268 if (newUrl) { 262 - await chrome.storage.local.set({ apiUrl: newUrl }); 263 269 apiUrl = newUrl; 264 - await sendMessage({ type: "UPDATE_SETTINGS" }); 265 - views.settings.style.display = "none"; 266 - checkSession(); 270 + } 271 + await sendMessage({ type: "UPDATE_SETTINGS" }); 272 + 273 + const tabs = await chrome.tabs.query({}); 274 + for (const tab of tabs) { 275 + if (tab.id) { 276 + try { 277 + await chrome.tabs.sendMessage(tab.id, { 278 + type: "UPDATE_OVERLAY_VISIBILITY", 279 + show: showOverlay, 280 + }); 281 + } catch { 282 + /* ignore */ 283 + } 284 + } 267 285 } 286 + 287 + views.settings.style.display = "none"; 288 + checkSession(); 268 289 }); 269 290 270 291 els.signOutBtn?.addEventListener("click", async () => { ··· 367 388 console.error("No currentUserDid, returning early"); 368 389 return; 369 390 } 370 - activeAnnotationUriForCollection = annotationUri; 371 391 views.collectionSelector.style.display = "flex"; 372 392 els.collectionList.innerHTML = ""; 373 393 els.collectionLoading.style.display = "block"; ··· 561 581 header.appendChild(badge); 562 582 } 563 583 564 - if ( 565 - item.author?.did === currentUserDid || 566 - item.creator?.did === currentUserDid 567 - ) { 584 + if (currentUserDid) { 568 585 const actions = document.createElement("div"); 569 586 actions.className = "annotation-item-actions"; 570 587 ··· 635 652 let hostname = item.source; 636 653 try { 637 654 hostname = new URL(item.source).hostname; 638 - } catch {} 655 + } catch { 656 + /* ignore */ 657 + } 639 658 640 659 const row = document.createElement("div"); 641 660 row.style.display = "flex"; ··· 658 677 659 678 row.appendChild(content); 660 679 661 - if ( 662 - item.author?.did === currentUserDid || 663 - item.creator?.did === currentUserDid 664 - ) { 680 + if (currentUserDid) { 665 681 const folderBtn = document.createElement("button"); 666 682 folderBtn.className = "btn-icon"; 667 683 folderBtn.innerHTML = ··· 701 717 let hostname = url; 702 718 try { 703 719 hostname = new URL(url).hostname; 704 - } catch {} 720 + } catch { 721 + /* ignore */ 722 + } 705 723 706 724 const header = document.createElement("div"); 707 725 header.className = "annotation-item-header"; ··· 721 739 722 740 header.appendChild(meta); 723 741 724 - if ( 725 - item.author?.did === currentUserDid || 726 - item.creator?.did === currentUserDid 727 - ) { 742 + if (currentUserDid) { 728 743 const actions = document.createElement("div"); 729 744 actions.className = "annotation-item-actions"; 730 745
+40
web/eslint.config.js
··· 1 + import js from "@eslint/js"; 2 + import globals from "globals"; 3 + import react from "eslint-plugin-react"; 4 + import reactHooks from "eslint-plugin-react-hooks"; 5 + import reactRefresh from "eslint-plugin-react-refresh"; 6 + 7 + export default [ 8 + { ignores: ["dist"] }, 9 + { 10 + files: ["**/*.{js,jsx}"], 11 + languageOptions: { 12 + ecmaVersion: 2020, 13 + globals: globals.browser, 14 + parserOptions: { 15 + ecmaVersion: "latest", 16 + ecmaFeatures: { jsx: true }, 17 + sourceType: "module", 18 + }, 19 + }, 20 + settings: { react: { version: "18.3" } }, 21 + plugins: { 22 + react, 23 + "react-hooks": reactHooks, 24 + "react-refresh": reactRefresh, 25 + }, 26 + rules: { 27 + ...js.configs.recommended.rules, 28 + ...react.configs.recommended.rules, 29 + ...react.configs["jsx-runtime"].rules, 30 + ...reactHooks.configs.recommended.rules, 31 + "react/jsx-no-target-blank": "off", 32 + "react-refresh/only-export-components": [ 33 + "warn", 34 + { allowConstantExport: true }, 35 + ], 36 + "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 37 + "react/prop-types": "off", 38 + }, 39 + }, 40 + ];
+3051 -12
web/package-lock.json
··· 15 15 "react-router-dom": "^6.28.0" 16 16 }, 17 17 "devDependencies": { 18 + "@eslint/js": "^9.39.2", 18 19 "@types/react": "^18.3.12", 19 20 "@types/react-dom": "^18.3.1", 20 21 "@vitejs/plugin-react": "^4.3.3", 22 + "eslint": "^9.39.2", 23 + "eslint-plugin-react": "^7.37.5", 24 + "eslint-plugin-react-hooks": "^7.0.1", 25 + "eslint-plugin-react-refresh": "^0.4.26", 26 + "globals": "^17.0.0", 21 27 "vite": "^6.0.3" 22 28 } 23 29 }, ··· 746 752 "node": ">=18" 747 753 } 748 754 }, 755 + "node_modules/@eslint-community/eslint-utils": { 756 + "version": "4.9.1", 757 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", 758 + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", 759 + "dev": true, 760 + "license": "MIT", 761 + "dependencies": { 762 + "eslint-visitor-keys": "^3.4.3" 763 + }, 764 + "engines": { 765 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 766 + }, 767 + "funding": { 768 + "url": "https://opencollective.com/eslint" 769 + }, 770 + "peerDependencies": { 771 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 772 + } 773 + }, 774 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 775 + "version": "3.4.3", 776 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 777 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 778 + "dev": true, 779 + "license": "Apache-2.0", 780 + "engines": { 781 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 782 + }, 783 + "funding": { 784 + "url": "https://opencollective.com/eslint" 785 + } 786 + }, 787 + "node_modules/@eslint-community/regexpp": { 788 + "version": "4.12.2", 789 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 790 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 791 + "dev": true, 792 + "license": "MIT", 793 + "engines": { 794 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 795 + } 796 + }, 797 + "node_modules/@eslint/config-array": { 798 + "version": "0.21.1", 799 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 800 + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 801 + "dev": true, 802 + "license": "Apache-2.0", 803 + "dependencies": { 804 + "@eslint/object-schema": "^2.1.7", 805 + "debug": "^4.3.1", 806 + "minimatch": "^3.1.2" 807 + }, 808 + "engines": { 809 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 810 + } 811 + }, 812 + "node_modules/@eslint/config-helpers": { 813 + "version": "0.4.2", 814 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 815 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 816 + "dev": true, 817 + "license": "Apache-2.0", 818 + "dependencies": { 819 + "@eslint/core": "^0.17.0" 820 + }, 821 + "engines": { 822 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 823 + } 824 + }, 825 + "node_modules/@eslint/core": { 826 + "version": "0.17.0", 827 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 828 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 829 + "dev": true, 830 + "license": "Apache-2.0", 831 + "dependencies": { 832 + "@types/json-schema": "^7.0.15" 833 + }, 834 + "engines": { 835 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 836 + } 837 + }, 838 + "node_modules/@eslint/eslintrc": { 839 + "version": "3.3.3", 840 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", 841 + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", 842 + "dev": true, 843 + "license": "MIT", 844 + "dependencies": { 845 + "ajv": "^6.12.4", 846 + "debug": "^4.3.2", 847 + "espree": "^10.0.1", 848 + "globals": "^14.0.0", 849 + "ignore": "^5.2.0", 850 + "import-fresh": "^3.2.1", 851 + "js-yaml": "^4.1.1", 852 + "minimatch": "^3.1.2", 853 + "strip-json-comments": "^3.1.1" 854 + }, 855 + "engines": { 856 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 857 + }, 858 + "funding": { 859 + "url": "https://opencollective.com/eslint" 860 + } 861 + }, 862 + "node_modules/@eslint/eslintrc/node_modules/globals": { 863 + "version": "14.0.0", 864 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 865 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 866 + "dev": true, 867 + "license": "MIT", 868 + "engines": { 869 + "node": ">=18" 870 + }, 871 + "funding": { 872 + "url": "https://github.com/sponsors/sindresorhus" 873 + } 874 + }, 875 + "node_modules/@eslint/js": { 876 + "version": "9.39.2", 877 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", 878 + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", 879 + "dev": true, 880 + "license": "MIT", 881 + "engines": { 882 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 883 + }, 884 + "funding": { 885 + "url": "https://eslint.org/donate" 886 + } 887 + }, 888 + "node_modules/@eslint/object-schema": { 889 + "version": "2.1.7", 890 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 891 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 892 + "dev": true, 893 + "license": "Apache-2.0", 894 + "engines": { 895 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 896 + } 897 + }, 898 + "node_modules/@eslint/plugin-kit": { 899 + "version": "0.4.1", 900 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 901 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 902 + "dev": true, 903 + "license": "Apache-2.0", 904 + "dependencies": { 905 + "@eslint/core": "^0.17.0", 906 + "levn": "^0.4.1" 907 + }, 908 + "engines": { 909 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 910 + } 911 + }, 912 + "node_modules/@humanfs/core": { 913 + "version": "0.19.1", 914 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 915 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 916 + "dev": true, 917 + "license": "Apache-2.0", 918 + "engines": { 919 + "node": ">=18.18.0" 920 + } 921 + }, 922 + "node_modules/@humanfs/node": { 923 + "version": "0.16.7", 924 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 925 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 926 + "dev": true, 927 + "license": "Apache-2.0", 928 + "dependencies": { 929 + "@humanfs/core": "^0.19.1", 930 + "@humanwhocodes/retry": "^0.4.0" 931 + }, 932 + "engines": { 933 + "node": ">=18.18.0" 934 + } 935 + }, 936 + "node_modules/@humanwhocodes/module-importer": { 937 + "version": "1.0.1", 938 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 939 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 940 + "dev": true, 941 + "license": "Apache-2.0", 942 + "engines": { 943 + "node": ">=12.22" 944 + }, 945 + "funding": { 946 + "type": "github", 947 + "url": "https://github.com/sponsors/nzakas" 948 + } 949 + }, 950 + "node_modules/@humanwhocodes/retry": { 951 + "version": "0.4.3", 952 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 953 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 954 + "dev": true, 955 + "license": "Apache-2.0", 956 + "engines": { 957 + "node": ">=18.18" 958 + }, 959 + "funding": { 960 + "type": "github", 961 + "url": "https://github.com/sponsors/nzakas" 962 + } 963 + }, 749 964 "node_modules/@jridgewell/gen-mapping": { 750 965 "version": "0.3.13", 751 966 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", ··· 797 1012 } 798 1013 }, 799 1014 "node_modules/@remix-run/router": { 800 - "version": "1.23.1", 801 - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", 802 - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", 1015 + "version": "1.23.2", 1016 + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", 1017 + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", 803 1018 "license": "MIT", 804 1019 "engines": { 805 1020 "node": ">=14.0.0" ··· 1172 1387 "dev": true, 1173 1388 "license": "MIT" 1174 1389 }, 1390 + "node_modules/@types/json-schema": { 1391 + "version": "7.0.15", 1392 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 1393 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 1394 + "dev": true, 1395 + "license": "MIT" 1396 + }, 1175 1397 "node_modules/@types/prop-types": { 1176 1398 "version": "15.7.15", 1177 1399 "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", ··· 1222 1444 "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 1223 1445 } 1224 1446 }, 1447 + "node_modules/acorn": { 1448 + "version": "8.15.0", 1449 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 1450 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 1451 + "dev": true, 1452 + "license": "MIT", 1453 + "peer": true, 1454 + "bin": { 1455 + "acorn": "bin/acorn" 1456 + }, 1457 + "engines": { 1458 + "node": ">=0.4.0" 1459 + } 1460 + }, 1461 + "node_modules/acorn-jsx": { 1462 + "version": "5.3.2", 1463 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1464 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1465 + "dev": true, 1466 + "license": "MIT", 1467 + "peerDependencies": { 1468 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 1469 + } 1470 + }, 1471 + "node_modules/ajv": { 1472 + "version": "6.12.6", 1473 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1474 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1475 + "dev": true, 1476 + "license": "MIT", 1477 + "dependencies": { 1478 + "fast-deep-equal": "^3.1.1", 1479 + "fast-json-stable-stringify": "^2.0.0", 1480 + "json-schema-traverse": "^0.4.1", 1481 + "uri-js": "^4.2.2" 1482 + }, 1483 + "funding": { 1484 + "type": "github", 1485 + "url": "https://github.com/sponsors/epoberezkin" 1486 + } 1487 + }, 1488 + "node_modules/ansi-styles": { 1489 + "version": "4.3.0", 1490 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1491 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1492 + "dev": true, 1493 + "license": "MIT", 1494 + "dependencies": { 1495 + "color-convert": "^2.0.1" 1496 + }, 1497 + "engines": { 1498 + "node": ">=8" 1499 + }, 1500 + "funding": { 1501 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1502 + } 1503 + }, 1504 + "node_modules/argparse": { 1505 + "version": "2.0.1", 1506 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1507 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1508 + "dev": true, 1509 + "license": "Python-2.0" 1510 + }, 1511 + "node_modules/array-buffer-byte-length": { 1512 + "version": "1.0.2", 1513 + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", 1514 + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", 1515 + "dev": true, 1516 + "license": "MIT", 1517 + "dependencies": { 1518 + "call-bound": "^1.0.3", 1519 + "is-array-buffer": "^3.0.5" 1520 + }, 1521 + "engines": { 1522 + "node": ">= 0.4" 1523 + }, 1524 + "funding": { 1525 + "url": "https://github.com/sponsors/ljharb" 1526 + } 1527 + }, 1528 + "node_modules/array-includes": { 1529 + "version": "3.1.9", 1530 + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", 1531 + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", 1532 + "dev": true, 1533 + "license": "MIT", 1534 + "dependencies": { 1535 + "call-bind": "^1.0.8", 1536 + "call-bound": "^1.0.4", 1537 + "define-properties": "^1.2.1", 1538 + "es-abstract": "^1.24.0", 1539 + "es-object-atoms": "^1.1.1", 1540 + "get-intrinsic": "^1.3.0", 1541 + "is-string": "^1.1.1", 1542 + "math-intrinsics": "^1.1.0" 1543 + }, 1544 + "engines": { 1545 + "node": ">= 0.4" 1546 + }, 1547 + "funding": { 1548 + "url": "https://github.com/sponsors/ljharb" 1549 + } 1550 + }, 1551 + "node_modules/array.prototype.findlast": { 1552 + "version": "1.2.5", 1553 + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", 1554 + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", 1555 + "dev": true, 1556 + "license": "MIT", 1557 + "dependencies": { 1558 + "call-bind": "^1.0.7", 1559 + "define-properties": "^1.2.1", 1560 + "es-abstract": "^1.23.2", 1561 + "es-errors": "^1.3.0", 1562 + "es-object-atoms": "^1.0.0", 1563 + "es-shim-unscopables": "^1.0.2" 1564 + }, 1565 + "engines": { 1566 + "node": ">= 0.4" 1567 + }, 1568 + "funding": { 1569 + "url": "https://github.com/sponsors/ljharb" 1570 + } 1571 + }, 1572 + "node_modules/array.prototype.flat": { 1573 + "version": "1.3.3", 1574 + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", 1575 + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", 1576 + "dev": true, 1577 + "license": "MIT", 1578 + "dependencies": { 1579 + "call-bind": "^1.0.8", 1580 + "define-properties": "^1.2.1", 1581 + "es-abstract": "^1.23.5", 1582 + "es-shim-unscopables": "^1.0.2" 1583 + }, 1584 + "engines": { 1585 + "node": ">= 0.4" 1586 + }, 1587 + "funding": { 1588 + "url": "https://github.com/sponsors/ljharb" 1589 + } 1590 + }, 1591 + "node_modules/array.prototype.flatmap": { 1592 + "version": "1.3.3", 1593 + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", 1594 + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", 1595 + "dev": true, 1596 + "license": "MIT", 1597 + "dependencies": { 1598 + "call-bind": "^1.0.8", 1599 + "define-properties": "^1.2.1", 1600 + "es-abstract": "^1.23.5", 1601 + "es-shim-unscopables": "^1.0.2" 1602 + }, 1603 + "engines": { 1604 + "node": ">= 0.4" 1605 + }, 1606 + "funding": { 1607 + "url": "https://github.com/sponsors/ljharb" 1608 + } 1609 + }, 1610 + "node_modules/array.prototype.tosorted": { 1611 + "version": "1.1.4", 1612 + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", 1613 + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", 1614 + "dev": true, 1615 + "license": "MIT", 1616 + "dependencies": { 1617 + "call-bind": "^1.0.7", 1618 + "define-properties": "^1.2.1", 1619 + "es-abstract": "^1.23.3", 1620 + "es-errors": "^1.3.0", 1621 + "es-shim-unscopables": "^1.0.2" 1622 + }, 1623 + "engines": { 1624 + "node": ">= 0.4" 1625 + } 1626 + }, 1627 + "node_modules/arraybuffer.prototype.slice": { 1628 + "version": "1.0.4", 1629 + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", 1630 + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", 1631 + "dev": true, 1632 + "license": "MIT", 1633 + "dependencies": { 1634 + "array-buffer-byte-length": "^1.0.1", 1635 + "call-bind": "^1.0.8", 1636 + "define-properties": "^1.2.1", 1637 + "es-abstract": "^1.23.5", 1638 + "es-errors": "^1.3.0", 1639 + "get-intrinsic": "^1.2.6", 1640 + "is-array-buffer": "^3.0.4" 1641 + }, 1642 + "engines": { 1643 + "node": ">= 0.4" 1644 + }, 1645 + "funding": { 1646 + "url": "https://github.com/sponsors/ljharb" 1647 + } 1648 + }, 1649 + "node_modules/async-function": { 1650 + "version": "1.0.0", 1651 + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", 1652 + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", 1653 + "dev": true, 1654 + "license": "MIT", 1655 + "engines": { 1656 + "node": ">= 0.4" 1657 + } 1658 + }, 1659 + "node_modules/available-typed-arrays": { 1660 + "version": "1.0.7", 1661 + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", 1662 + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", 1663 + "dev": true, 1664 + "license": "MIT", 1665 + "dependencies": { 1666 + "possible-typed-array-names": "^1.0.0" 1667 + }, 1668 + "engines": { 1669 + "node": ">= 0.4" 1670 + }, 1671 + "funding": { 1672 + "url": "https://github.com/sponsors/ljharb" 1673 + } 1674 + }, 1675 + "node_modules/balanced-match": { 1676 + "version": "1.0.2", 1677 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1678 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1679 + "dev": true, 1680 + "license": "MIT" 1681 + }, 1225 1682 "node_modules/baseline-browser-mapping": { 1226 1683 "version": "2.9.11", 1227 1684 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", ··· 1230 1687 "license": "Apache-2.0", 1231 1688 "bin": { 1232 1689 "baseline-browser-mapping": "dist/cli.js" 1690 + } 1691 + }, 1692 + "node_modules/brace-expansion": { 1693 + "version": "1.1.12", 1694 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 1695 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 1696 + "dev": true, 1697 + "license": "MIT", 1698 + "dependencies": { 1699 + "balanced-match": "^1.0.0", 1700 + "concat-map": "0.0.1" 1233 1701 } 1234 1702 }, 1235 1703 "node_modules/browserslist": { ··· 1267 1735 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1268 1736 } 1269 1737 }, 1738 + "node_modules/call-bind": { 1739 + "version": "1.0.8", 1740 + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", 1741 + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", 1742 + "dev": true, 1743 + "license": "MIT", 1744 + "dependencies": { 1745 + "call-bind-apply-helpers": "^1.0.0", 1746 + "es-define-property": "^1.0.0", 1747 + "get-intrinsic": "^1.2.4", 1748 + "set-function-length": "^1.2.2" 1749 + }, 1750 + "engines": { 1751 + "node": ">= 0.4" 1752 + }, 1753 + "funding": { 1754 + "url": "https://github.com/sponsors/ljharb" 1755 + } 1756 + }, 1757 + "node_modules/call-bind-apply-helpers": { 1758 + "version": "1.0.2", 1759 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 1760 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 1761 + "dev": true, 1762 + "license": "MIT", 1763 + "dependencies": { 1764 + "es-errors": "^1.3.0", 1765 + "function-bind": "^1.1.2" 1766 + }, 1767 + "engines": { 1768 + "node": ">= 0.4" 1769 + } 1770 + }, 1771 + "node_modules/call-bound": { 1772 + "version": "1.0.4", 1773 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 1774 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 1775 + "dev": true, 1776 + "license": "MIT", 1777 + "dependencies": { 1778 + "call-bind-apply-helpers": "^1.0.2", 1779 + "get-intrinsic": "^1.3.0" 1780 + }, 1781 + "engines": { 1782 + "node": ">= 0.4" 1783 + }, 1784 + "funding": { 1785 + "url": "https://github.com/sponsors/ljharb" 1786 + } 1787 + }, 1788 + "node_modules/callsites": { 1789 + "version": "3.1.0", 1790 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1791 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1792 + "dev": true, 1793 + "license": "MIT", 1794 + "engines": { 1795 + "node": ">=6" 1796 + } 1797 + }, 1270 1798 "node_modules/caniuse-lite": { 1271 1799 "version": "1.0.30001762", 1272 1800 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", ··· 1288 1816 ], 1289 1817 "license": "CC-BY-4.0" 1290 1818 }, 1819 + "node_modules/chalk": { 1820 + "version": "4.1.2", 1821 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1822 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1823 + "dev": true, 1824 + "license": "MIT", 1825 + "dependencies": { 1826 + "ansi-styles": "^4.1.0", 1827 + "supports-color": "^7.1.0" 1828 + }, 1829 + "engines": { 1830 + "node": ">=10" 1831 + }, 1832 + "funding": { 1833 + "url": "https://github.com/chalk/chalk?sponsor=1" 1834 + } 1835 + }, 1836 + "node_modules/color-convert": { 1837 + "version": "2.0.1", 1838 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1839 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1840 + "dev": true, 1841 + "license": "MIT", 1842 + "dependencies": { 1843 + "color-name": "~1.1.4" 1844 + }, 1845 + "engines": { 1846 + "node": ">=7.0.0" 1847 + } 1848 + }, 1849 + "node_modules/color-name": { 1850 + "version": "1.1.4", 1851 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1852 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1853 + "dev": true, 1854 + "license": "MIT" 1855 + }, 1856 + "node_modules/concat-map": { 1857 + "version": "0.0.1", 1858 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1859 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1860 + "dev": true, 1861 + "license": "MIT" 1862 + }, 1291 1863 "node_modules/convert-source-map": { 1292 1864 "version": "2.0.0", 1293 1865 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", ··· 1295 1867 "dev": true, 1296 1868 "license": "MIT" 1297 1869 }, 1870 + "node_modules/cross-spawn": { 1871 + "version": "7.0.6", 1872 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 1873 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 1874 + "dev": true, 1875 + "license": "MIT", 1876 + "dependencies": { 1877 + "path-key": "^3.1.0", 1878 + "shebang-command": "^2.0.0", 1879 + "which": "^2.0.1" 1880 + }, 1881 + "engines": { 1882 + "node": ">= 8" 1883 + } 1884 + }, 1298 1885 "node_modules/csstype": { 1299 1886 "version": "3.2.3", 1300 1887 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", ··· 1302 1889 "dev": true, 1303 1890 "license": "MIT" 1304 1891 }, 1892 + "node_modules/data-view-buffer": { 1893 + "version": "1.0.2", 1894 + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", 1895 + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", 1896 + "dev": true, 1897 + "license": "MIT", 1898 + "dependencies": { 1899 + "call-bound": "^1.0.3", 1900 + "es-errors": "^1.3.0", 1901 + "is-data-view": "^1.0.2" 1902 + }, 1903 + "engines": { 1904 + "node": ">= 0.4" 1905 + }, 1906 + "funding": { 1907 + "url": "https://github.com/sponsors/ljharb" 1908 + } 1909 + }, 1910 + "node_modules/data-view-byte-length": { 1911 + "version": "1.0.2", 1912 + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", 1913 + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", 1914 + "dev": true, 1915 + "license": "MIT", 1916 + "dependencies": { 1917 + "call-bound": "^1.0.3", 1918 + "es-errors": "^1.3.0", 1919 + "is-data-view": "^1.0.2" 1920 + }, 1921 + "engines": { 1922 + "node": ">= 0.4" 1923 + }, 1924 + "funding": { 1925 + "url": "https://github.com/sponsors/inspect-js" 1926 + } 1927 + }, 1928 + "node_modules/data-view-byte-offset": { 1929 + "version": "1.0.1", 1930 + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", 1931 + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", 1932 + "dev": true, 1933 + "license": "MIT", 1934 + "dependencies": { 1935 + "call-bound": "^1.0.2", 1936 + "es-errors": "^1.3.0", 1937 + "is-data-view": "^1.0.1" 1938 + }, 1939 + "engines": { 1940 + "node": ">= 0.4" 1941 + }, 1942 + "funding": { 1943 + "url": "https://github.com/sponsors/ljharb" 1944 + } 1945 + }, 1305 1946 "node_modules/debug": { 1306 1947 "version": "4.4.3", 1307 1948 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", ··· 1320 1961 } 1321 1962 } 1322 1963 }, 1964 + "node_modules/deep-is": { 1965 + "version": "0.1.4", 1966 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 1967 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 1968 + "dev": true, 1969 + "license": "MIT" 1970 + }, 1971 + "node_modules/define-data-property": { 1972 + "version": "1.1.4", 1973 + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 1974 + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 1975 + "dev": true, 1976 + "license": "MIT", 1977 + "dependencies": { 1978 + "es-define-property": "^1.0.0", 1979 + "es-errors": "^1.3.0", 1980 + "gopd": "^1.0.1" 1981 + }, 1982 + "engines": { 1983 + "node": ">= 0.4" 1984 + }, 1985 + "funding": { 1986 + "url": "https://github.com/sponsors/ljharb" 1987 + } 1988 + }, 1989 + "node_modules/define-properties": { 1990 + "version": "1.2.1", 1991 + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", 1992 + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", 1993 + "dev": true, 1994 + "license": "MIT", 1995 + "dependencies": { 1996 + "define-data-property": "^1.0.1", 1997 + "has-property-descriptors": "^1.0.0", 1998 + "object-keys": "^1.1.1" 1999 + }, 2000 + "engines": { 2001 + "node": ">= 0.4" 2002 + }, 2003 + "funding": { 2004 + "url": "https://github.com/sponsors/ljharb" 2005 + } 2006 + }, 2007 + "node_modules/doctrine": { 2008 + "version": "2.1.0", 2009 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 2010 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 2011 + "dev": true, 2012 + "license": "Apache-2.0", 2013 + "dependencies": { 2014 + "esutils": "^2.0.2" 2015 + }, 2016 + "engines": { 2017 + "node": ">=0.10.0" 2018 + } 2019 + }, 2020 + "node_modules/dunder-proto": { 2021 + "version": "1.0.1", 2022 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 2023 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 2024 + "dev": true, 2025 + "license": "MIT", 2026 + "dependencies": { 2027 + "call-bind-apply-helpers": "^1.0.1", 2028 + "es-errors": "^1.3.0", 2029 + "gopd": "^1.2.0" 2030 + }, 2031 + "engines": { 2032 + "node": ">= 0.4" 2033 + } 2034 + }, 1323 2035 "node_modules/electron-to-chromium": { 1324 2036 "version": "1.5.267", 1325 2037 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", ··· 1327 2039 "dev": true, 1328 2040 "license": "ISC" 1329 2041 }, 2042 + "node_modules/es-abstract": { 2043 + "version": "1.24.1", 2044 + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", 2045 + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", 2046 + "dev": true, 2047 + "license": "MIT", 2048 + "dependencies": { 2049 + "array-buffer-byte-length": "^1.0.2", 2050 + "arraybuffer.prototype.slice": "^1.0.4", 2051 + "available-typed-arrays": "^1.0.7", 2052 + "call-bind": "^1.0.8", 2053 + "call-bound": "^1.0.4", 2054 + "data-view-buffer": "^1.0.2", 2055 + "data-view-byte-length": "^1.0.2", 2056 + "data-view-byte-offset": "^1.0.1", 2057 + "es-define-property": "^1.0.1", 2058 + "es-errors": "^1.3.0", 2059 + "es-object-atoms": "^1.1.1", 2060 + "es-set-tostringtag": "^2.1.0", 2061 + "es-to-primitive": "^1.3.0", 2062 + "function.prototype.name": "^1.1.8", 2063 + "get-intrinsic": "^1.3.0", 2064 + "get-proto": "^1.0.1", 2065 + "get-symbol-description": "^1.1.0", 2066 + "globalthis": "^1.0.4", 2067 + "gopd": "^1.2.0", 2068 + "has-property-descriptors": "^1.0.2", 2069 + "has-proto": "^1.2.0", 2070 + "has-symbols": "^1.1.0", 2071 + "hasown": "^2.0.2", 2072 + "internal-slot": "^1.1.0", 2073 + "is-array-buffer": "^3.0.5", 2074 + "is-callable": "^1.2.7", 2075 + "is-data-view": "^1.0.2", 2076 + "is-negative-zero": "^2.0.3", 2077 + "is-regex": "^1.2.1", 2078 + "is-set": "^2.0.3", 2079 + "is-shared-array-buffer": "^1.0.4", 2080 + "is-string": "^1.1.1", 2081 + "is-typed-array": "^1.1.15", 2082 + "is-weakref": "^1.1.1", 2083 + "math-intrinsics": "^1.1.0", 2084 + "object-inspect": "^1.13.4", 2085 + "object-keys": "^1.1.1", 2086 + "object.assign": "^4.1.7", 2087 + "own-keys": "^1.0.1", 2088 + "regexp.prototype.flags": "^1.5.4", 2089 + "safe-array-concat": "^1.1.3", 2090 + "safe-push-apply": "^1.0.0", 2091 + "safe-regex-test": "^1.1.0", 2092 + "set-proto": "^1.0.0", 2093 + "stop-iteration-iterator": "^1.1.0", 2094 + "string.prototype.trim": "^1.2.10", 2095 + "string.prototype.trimend": "^1.0.9", 2096 + "string.prototype.trimstart": "^1.0.8", 2097 + "typed-array-buffer": "^1.0.3", 2098 + "typed-array-byte-length": "^1.0.3", 2099 + "typed-array-byte-offset": "^1.0.4", 2100 + "typed-array-length": "^1.0.7", 2101 + "unbox-primitive": "^1.1.0", 2102 + "which-typed-array": "^1.1.19" 2103 + }, 2104 + "engines": { 2105 + "node": ">= 0.4" 2106 + }, 2107 + "funding": { 2108 + "url": "https://github.com/sponsors/ljharb" 2109 + } 2110 + }, 2111 + "node_modules/es-define-property": { 2112 + "version": "1.0.1", 2113 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 2114 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 2115 + "dev": true, 2116 + "license": "MIT", 2117 + "engines": { 2118 + "node": ">= 0.4" 2119 + } 2120 + }, 2121 + "node_modules/es-errors": { 2122 + "version": "1.3.0", 2123 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 2124 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 2125 + "dev": true, 2126 + "license": "MIT", 2127 + "engines": { 2128 + "node": ">= 0.4" 2129 + } 2130 + }, 2131 + "node_modules/es-iterator-helpers": { 2132 + "version": "1.2.2", 2133 + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", 2134 + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", 2135 + "dev": true, 2136 + "license": "MIT", 2137 + "dependencies": { 2138 + "call-bind": "^1.0.8", 2139 + "call-bound": "^1.0.4", 2140 + "define-properties": "^1.2.1", 2141 + "es-abstract": "^1.24.1", 2142 + "es-errors": "^1.3.0", 2143 + "es-set-tostringtag": "^2.1.0", 2144 + "function-bind": "^1.1.2", 2145 + "get-intrinsic": "^1.3.0", 2146 + "globalthis": "^1.0.4", 2147 + "gopd": "^1.2.0", 2148 + "has-property-descriptors": "^1.0.2", 2149 + "has-proto": "^1.2.0", 2150 + "has-symbols": "^1.1.0", 2151 + "internal-slot": "^1.1.0", 2152 + "iterator.prototype": "^1.1.5", 2153 + "safe-array-concat": "^1.1.3" 2154 + }, 2155 + "engines": { 2156 + "node": ">= 0.4" 2157 + } 2158 + }, 2159 + "node_modules/es-object-atoms": { 2160 + "version": "1.1.1", 2161 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 2162 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 2163 + "dev": true, 2164 + "license": "MIT", 2165 + "dependencies": { 2166 + "es-errors": "^1.3.0" 2167 + }, 2168 + "engines": { 2169 + "node": ">= 0.4" 2170 + } 2171 + }, 2172 + "node_modules/es-set-tostringtag": { 2173 + "version": "2.1.0", 2174 + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 2175 + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 2176 + "dev": true, 2177 + "license": "MIT", 2178 + "dependencies": { 2179 + "es-errors": "^1.3.0", 2180 + "get-intrinsic": "^1.2.6", 2181 + "has-tostringtag": "^1.0.2", 2182 + "hasown": "^2.0.2" 2183 + }, 2184 + "engines": { 2185 + "node": ">= 0.4" 2186 + } 2187 + }, 2188 + "node_modules/es-shim-unscopables": { 2189 + "version": "1.1.0", 2190 + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", 2191 + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", 2192 + "dev": true, 2193 + "license": "MIT", 2194 + "dependencies": { 2195 + "hasown": "^2.0.2" 2196 + }, 2197 + "engines": { 2198 + "node": ">= 0.4" 2199 + } 2200 + }, 2201 + "node_modules/es-to-primitive": { 2202 + "version": "1.3.0", 2203 + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", 2204 + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", 2205 + "dev": true, 2206 + "license": "MIT", 2207 + "dependencies": { 2208 + "is-callable": "^1.2.7", 2209 + "is-date-object": "^1.0.5", 2210 + "is-symbol": "^1.0.4" 2211 + }, 2212 + "engines": { 2213 + "node": ">= 0.4" 2214 + }, 2215 + "funding": { 2216 + "url": "https://github.com/sponsors/ljharb" 2217 + } 2218 + }, 1330 2219 "node_modules/esbuild": { 1331 2220 "version": "0.25.12", 1332 2221 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", ··· 1379 2268 "node": ">=6" 1380 2269 } 1381 2270 }, 2271 + "node_modules/escape-string-regexp": { 2272 + "version": "4.0.0", 2273 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 2274 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 2275 + "dev": true, 2276 + "license": "MIT", 2277 + "engines": { 2278 + "node": ">=10" 2279 + }, 2280 + "funding": { 2281 + "url": "https://github.com/sponsors/sindresorhus" 2282 + } 2283 + }, 2284 + "node_modules/eslint": { 2285 + "version": "9.39.2", 2286 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", 2287 + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 2288 + "dev": true, 2289 + "license": "MIT", 2290 + "peer": true, 2291 + "dependencies": { 2292 + "@eslint-community/eslint-utils": "^4.8.0", 2293 + "@eslint-community/regexpp": "^4.12.1", 2294 + "@eslint/config-array": "^0.21.1", 2295 + "@eslint/config-helpers": "^0.4.2", 2296 + "@eslint/core": "^0.17.0", 2297 + "@eslint/eslintrc": "^3.3.1", 2298 + "@eslint/js": "9.39.2", 2299 + "@eslint/plugin-kit": "^0.4.1", 2300 + "@humanfs/node": "^0.16.6", 2301 + "@humanwhocodes/module-importer": "^1.0.1", 2302 + "@humanwhocodes/retry": "^0.4.2", 2303 + "@types/estree": "^1.0.6", 2304 + "ajv": "^6.12.4", 2305 + "chalk": "^4.0.0", 2306 + "cross-spawn": "^7.0.6", 2307 + "debug": "^4.3.2", 2308 + "escape-string-regexp": "^4.0.0", 2309 + "eslint-scope": "^8.4.0", 2310 + "eslint-visitor-keys": "^4.2.1", 2311 + "espree": "^10.4.0", 2312 + "esquery": "^1.5.0", 2313 + "esutils": "^2.0.2", 2314 + "fast-deep-equal": "^3.1.3", 2315 + "file-entry-cache": "^8.0.0", 2316 + "find-up": "^5.0.0", 2317 + "glob-parent": "^6.0.2", 2318 + "ignore": "^5.2.0", 2319 + "imurmurhash": "^0.1.4", 2320 + "is-glob": "^4.0.0", 2321 + "json-stable-stringify-without-jsonify": "^1.0.1", 2322 + "lodash.merge": "^4.6.2", 2323 + "minimatch": "^3.1.2", 2324 + "natural-compare": "^1.4.0", 2325 + "optionator": "^0.9.3" 2326 + }, 2327 + "bin": { 2328 + "eslint": "bin/eslint.js" 2329 + }, 2330 + "engines": { 2331 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2332 + }, 2333 + "funding": { 2334 + "url": "https://eslint.org/donate" 2335 + }, 2336 + "peerDependencies": { 2337 + "jiti": "*" 2338 + }, 2339 + "peerDependenciesMeta": { 2340 + "jiti": { 2341 + "optional": true 2342 + } 2343 + } 2344 + }, 2345 + "node_modules/eslint-plugin-react": { 2346 + "version": "7.37.5", 2347 + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", 2348 + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", 2349 + "dev": true, 2350 + "license": "MIT", 2351 + "dependencies": { 2352 + "array-includes": "^3.1.8", 2353 + "array.prototype.findlast": "^1.2.5", 2354 + "array.prototype.flatmap": "^1.3.3", 2355 + "array.prototype.tosorted": "^1.1.4", 2356 + "doctrine": "^2.1.0", 2357 + "es-iterator-helpers": "^1.2.1", 2358 + "estraverse": "^5.3.0", 2359 + "hasown": "^2.0.2", 2360 + "jsx-ast-utils": "^2.4.1 || ^3.0.0", 2361 + "minimatch": "^3.1.2", 2362 + "object.entries": "^1.1.9", 2363 + "object.fromentries": "^2.0.8", 2364 + "object.values": "^1.2.1", 2365 + "prop-types": "^15.8.1", 2366 + "resolve": "^2.0.0-next.5", 2367 + "semver": "^6.3.1", 2368 + "string.prototype.matchall": "^4.0.12", 2369 + "string.prototype.repeat": "^1.0.0" 2370 + }, 2371 + "engines": { 2372 + "node": ">=4" 2373 + }, 2374 + "peerDependencies": { 2375 + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" 2376 + } 2377 + }, 2378 + "node_modules/eslint-plugin-react-hooks": { 2379 + "version": "7.0.1", 2380 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", 2381 + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", 2382 + "dev": true, 2383 + "license": "MIT", 2384 + "dependencies": { 2385 + "@babel/core": "^7.24.4", 2386 + "@babel/parser": "^7.24.4", 2387 + "hermes-parser": "^0.25.1", 2388 + "zod": "^3.25.0 || ^4.0.0", 2389 + "zod-validation-error": "^3.5.0 || ^4.0.0" 2390 + }, 2391 + "engines": { 2392 + "node": ">=18" 2393 + }, 2394 + "peerDependencies": { 2395 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 2396 + } 2397 + }, 2398 + "node_modules/eslint-plugin-react-refresh": { 2399 + "version": "0.4.26", 2400 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", 2401 + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", 2402 + "dev": true, 2403 + "license": "MIT", 2404 + "peerDependencies": { 2405 + "eslint": ">=8.40" 2406 + } 2407 + }, 2408 + "node_modules/eslint-scope": { 2409 + "version": "8.4.0", 2410 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 2411 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 2412 + "dev": true, 2413 + "license": "BSD-2-Clause", 2414 + "dependencies": { 2415 + "esrecurse": "^4.3.0", 2416 + "estraverse": "^5.2.0" 2417 + }, 2418 + "engines": { 2419 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2420 + }, 2421 + "funding": { 2422 + "url": "https://opencollective.com/eslint" 2423 + } 2424 + }, 2425 + "node_modules/eslint-visitor-keys": { 2426 + "version": "4.2.1", 2427 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 2428 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 2429 + "dev": true, 2430 + "license": "Apache-2.0", 2431 + "engines": { 2432 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2433 + }, 2434 + "funding": { 2435 + "url": "https://opencollective.com/eslint" 2436 + } 2437 + }, 2438 + "node_modules/espree": { 2439 + "version": "10.4.0", 2440 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 2441 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 2442 + "dev": true, 2443 + "license": "BSD-2-Clause", 2444 + "dependencies": { 2445 + "acorn": "^8.15.0", 2446 + "acorn-jsx": "^5.3.2", 2447 + "eslint-visitor-keys": "^4.2.1" 2448 + }, 2449 + "engines": { 2450 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2451 + }, 2452 + "funding": { 2453 + "url": "https://opencollective.com/eslint" 2454 + } 2455 + }, 2456 + "node_modules/esquery": { 2457 + "version": "1.7.0", 2458 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", 2459 + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", 2460 + "dev": true, 2461 + "license": "BSD-3-Clause", 2462 + "dependencies": { 2463 + "estraverse": "^5.1.0" 2464 + }, 2465 + "engines": { 2466 + "node": ">=0.10" 2467 + } 2468 + }, 2469 + "node_modules/esrecurse": { 2470 + "version": "4.3.0", 2471 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 2472 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 2473 + "dev": true, 2474 + "license": "BSD-2-Clause", 2475 + "dependencies": { 2476 + "estraverse": "^5.2.0" 2477 + }, 2478 + "engines": { 2479 + "node": ">=4.0" 2480 + } 2481 + }, 2482 + "node_modules/estraverse": { 2483 + "version": "5.3.0", 2484 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2485 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2486 + "dev": true, 2487 + "license": "BSD-2-Clause", 2488 + "engines": { 2489 + "node": ">=4.0" 2490 + } 2491 + }, 2492 + "node_modules/esutils": { 2493 + "version": "2.0.3", 2494 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2495 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2496 + "dev": true, 2497 + "license": "BSD-2-Clause", 2498 + "engines": { 2499 + "node": ">=0.10.0" 2500 + } 2501 + }, 2502 + "node_modules/fast-deep-equal": { 2503 + "version": "3.1.3", 2504 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2505 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2506 + "dev": true, 2507 + "license": "MIT" 2508 + }, 2509 + "node_modules/fast-json-stable-stringify": { 2510 + "version": "2.1.0", 2511 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 2512 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 2513 + "dev": true, 2514 + "license": "MIT" 2515 + }, 2516 + "node_modules/fast-levenshtein": { 2517 + "version": "2.0.6", 2518 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 2519 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 2520 + "dev": true, 2521 + "license": "MIT" 2522 + }, 1382 2523 "node_modules/fdir": { 1383 2524 "version": "6.5.0", 1384 2525 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", ··· 1397 2538 } 1398 2539 } 1399 2540 }, 2541 + "node_modules/file-entry-cache": { 2542 + "version": "8.0.0", 2543 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 2544 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 2545 + "dev": true, 2546 + "license": "MIT", 2547 + "dependencies": { 2548 + "flat-cache": "^4.0.0" 2549 + }, 2550 + "engines": { 2551 + "node": ">=16.0.0" 2552 + } 2553 + }, 2554 + "node_modules/find-up": { 2555 + "version": "5.0.0", 2556 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 2557 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 2558 + "dev": true, 2559 + "license": "MIT", 2560 + "dependencies": { 2561 + "locate-path": "^6.0.0", 2562 + "path-exists": "^4.0.0" 2563 + }, 2564 + "engines": { 2565 + "node": ">=10" 2566 + }, 2567 + "funding": { 2568 + "url": "https://github.com/sponsors/sindresorhus" 2569 + } 2570 + }, 2571 + "node_modules/flat-cache": { 2572 + "version": "4.0.1", 2573 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 2574 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 2575 + "dev": true, 2576 + "license": "MIT", 2577 + "dependencies": { 2578 + "flatted": "^3.2.9", 2579 + "keyv": "^4.5.4" 2580 + }, 2581 + "engines": { 2582 + "node": ">=16" 2583 + } 2584 + }, 2585 + "node_modules/flatted": { 2586 + "version": "3.3.3", 2587 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 2588 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 2589 + "dev": true, 2590 + "license": "ISC" 2591 + }, 2592 + "node_modules/for-each": { 2593 + "version": "0.3.5", 2594 + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", 2595 + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", 2596 + "dev": true, 2597 + "license": "MIT", 2598 + "dependencies": { 2599 + "is-callable": "^1.2.7" 2600 + }, 2601 + "engines": { 2602 + "node": ">= 0.4" 2603 + }, 2604 + "funding": { 2605 + "url": "https://github.com/sponsors/ljharb" 2606 + } 2607 + }, 1400 2608 "node_modules/fsevents": { 1401 2609 "version": "2.3.3", 1402 2610 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 1412 2620 "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1413 2621 } 1414 2622 }, 2623 + "node_modules/function-bind": { 2624 + "version": "1.1.2", 2625 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 2626 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 2627 + "dev": true, 2628 + "license": "MIT", 2629 + "funding": { 2630 + "url": "https://github.com/sponsors/ljharb" 2631 + } 2632 + }, 2633 + "node_modules/function.prototype.name": { 2634 + "version": "1.1.8", 2635 + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", 2636 + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", 2637 + "dev": true, 2638 + "license": "MIT", 2639 + "dependencies": { 2640 + "call-bind": "^1.0.8", 2641 + "call-bound": "^1.0.3", 2642 + "define-properties": "^1.2.1", 2643 + "functions-have-names": "^1.2.3", 2644 + "hasown": "^2.0.2", 2645 + "is-callable": "^1.2.7" 2646 + }, 2647 + "engines": { 2648 + "node": ">= 0.4" 2649 + }, 2650 + "funding": { 2651 + "url": "https://github.com/sponsors/ljharb" 2652 + } 2653 + }, 2654 + "node_modules/functions-have-names": { 2655 + "version": "1.2.3", 2656 + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", 2657 + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", 2658 + "dev": true, 2659 + "license": "MIT", 2660 + "funding": { 2661 + "url": "https://github.com/sponsors/ljharb" 2662 + } 2663 + }, 2664 + "node_modules/generator-function": { 2665 + "version": "2.0.1", 2666 + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", 2667 + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", 2668 + "dev": true, 2669 + "license": "MIT", 2670 + "engines": { 2671 + "node": ">= 0.4" 2672 + } 2673 + }, 1415 2674 "node_modules/gensync": { 1416 2675 "version": "1.0.0-beta.2", 1417 2676 "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", ··· 1422 2681 "node": ">=6.9.0" 1423 2682 } 1424 2683 }, 2684 + "node_modules/get-intrinsic": { 2685 + "version": "1.3.0", 2686 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 2687 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 2688 + "dev": true, 2689 + "license": "MIT", 2690 + "dependencies": { 2691 + "call-bind-apply-helpers": "^1.0.2", 2692 + "es-define-property": "^1.0.1", 2693 + "es-errors": "^1.3.0", 2694 + "es-object-atoms": "^1.1.1", 2695 + "function-bind": "^1.1.2", 2696 + "get-proto": "^1.0.1", 2697 + "gopd": "^1.2.0", 2698 + "has-symbols": "^1.1.0", 2699 + "hasown": "^2.0.2", 2700 + "math-intrinsics": "^1.1.0" 2701 + }, 2702 + "engines": { 2703 + "node": ">= 0.4" 2704 + }, 2705 + "funding": { 2706 + "url": "https://github.com/sponsors/ljharb" 2707 + } 2708 + }, 2709 + "node_modules/get-proto": { 2710 + "version": "1.0.1", 2711 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 2712 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 2713 + "dev": true, 2714 + "license": "MIT", 2715 + "dependencies": { 2716 + "dunder-proto": "^1.0.1", 2717 + "es-object-atoms": "^1.0.0" 2718 + }, 2719 + "engines": { 2720 + "node": ">= 0.4" 2721 + } 2722 + }, 2723 + "node_modules/get-symbol-description": { 2724 + "version": "1.1.0", 2725 + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", 2726 + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", 2727 + "dev": true, 2728 + "license": "MIT", 2729 + "dependencies": { 2730 + "call-bound": "^1.0.3", 2731 + "es-errors": "^1.3.0", 2732 + "get-intrinsic": "^1.2.6" 2733 + }, 2734 + "engines": { 2735 + "node": ">= 0.4" 2736 + }, 2737 + "funding": { 2738 + "url": "https://github.com/sponsors/ljharb" 2739 + } 2740 + }, 2741 + "node_modules/glob-parent": { 2742 + "version": "6.0.2", 2743 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 2744 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 2745 + "dev": true, 2746 + "license": "ISC", 2747 + "dependencies": { 2748 + "is-glob": "^4.0.3" 2749 + }, 2750 + "engines": { 2751 + "node": ">=10.13.0" 2752 + } 2753 + }, 2754 + "node_modules/globals": { 2755 + "version": "17.0.0", 2756 + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", 2757 + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", 2758 + "dev": true, 2759 + "license": "MIT", 2760 + "engines": { 2761 + "node": ">=18" 2762 + }, 2763 + "funding": { 2764 + "url": "https://github.com/sponsors/sindresorhus" 2765 + } 2766 + }, 2767 + "node_modules/globalthis": { 2768 + "version": "1.0.4", 2769 + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", 2770 + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", 2771 + "dev": true, 2772 + "license": "MIT", 2773 + "dependencies": { 2774 + "define-properties": "^1.2.1", 2775 + "gopd": "^1.0.1" 2776 + }, 2777 + "engines": { 2778 + "node": ">= 0.4" 2779 + }, 2780 + "funding": { 2781 + "url": "https://github.com/sponsors/ljharb" 2782 + } 2783 + }, 2784 + "node_modules/gopd": { 2785 + "version": "1.2.0", 2786 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 2787 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 2788 + "dev": true, 2789 + "license": "MIT", 2790 + "engines": { 2791 + "node": ">= 0.4" 2792 + }, 2793 + "funding": { 2794 + "url": "https://github.com/sponsors/ljharb" 2795 + } 2796 + }, 2797 + "node_modules/has-bigints": { 2798 + "version": "1.1.0", 2799 + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", 2800 + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", 2801 + "dev": true, 2802 + "license": "MIT", 2803 + "engines": { 2804 + "node": ">= 0.4" 2805 + }, 2806 + "funding": { 2807 + "url": "https://github.com/sponsors/ljharb" 2808 + } 2809 + }, 2810 + "node_modules/has-flag": { 2811 + "version": "4.0.0", 2812 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2813 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2814 + "dev": true, 2815 + "license": "MIT", 2816 + "engines": { 2817 + "node": ">=8" 2818 + } 2819 + }, 2820 + "node_modules/has-property-descriptors": { 2821 + "version": "1.0.2", 2822 + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 2823 + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 2824 + "dev": true, 2825 + "license": "MIT", 2826 + "dependencies": { 2827 + "es-define-property": "^1.0.0" 2828 + }, 2829 + "funding": { 2830 + "url": "https://github.com/sponsors/ljharb" 2831 + } 2832 + }, 2833 + "node_modules/has-proto": { 2834 + "version": "1.2.0", 2835 + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", 2836 + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", 2837 + "dev": true, 2838 + "license": "MIT", 2839 + "dependencies": { 2840 + "dunder-proto": "^1.0.0" 2841 + }, 2842 + "engines": { 2843 + "node": ">= 0.4" 2844 + }, 2845 + "funding": { 2846 + "url": "https://github.com/sponsors/ljharb" 2847 + } 2848 + }, 2849 + "node_modules/has-symbols": { 2850 + "version": "1.1.0", 2851 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 2852 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 2853 + "dev": true, 2854 + "license": "MIT", 2855 + "engines": { 2856 + "node": ">= 0.4" 2857 + }, 2858 + "funding": { 2859 + "url": "https://github.com/sponsors/ljharb" 2860 + } 2861 + }, 2862 + "node_modules/has-tostringtag": { 2863 + "version": "1.0.2", 2864 + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 2865 + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 2866 + "dev": true, 2867 + "license": "MIT", 2868 + "dependencies": { 2869 + "has-symbols": "^1.0.3" 2870 + }, 2871 + "engines": { 2872 + "node": ">= 0.4" 2873 + }, 2874 + "funding": { 2875 + "url": "https://github.com/sponsors/ljharb" 2876 + } 2877 + }, 2878 + "node_modules/hasown": { 2879 + "version": "2.0.2", 2880 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 2881 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 2882 + "dev": true, 2883 + "license": "MIT", 2884 + "dependencies": { 2885 + "function-bind": "^1.1.2" 2886 + }, 2887 + "engines": { 2888 + "node": ">= 0.4" 2889 + } 2890 + }, 2891 + "node_modules/hermes-estree": { 2892 + "version": "0.25.1", 2893 + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", 2894 + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", 2895 + "dev": true, 2896 + "license": "MIT" 2897 + }, 2898 + "node_modules/hermes-parser": { 2899 + "version": "0.25.1", 2900 + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", 2901 + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", 2902 + "dev": true, 2903 + "license": "MIT", 2904 + "dependencies": { 2905 + "hermes-estree": "0.25.1" 2906 + } 2907 + }, 2908 + "node_modules/ignore": { 2909 + "version": "5.3.2", 2910 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 2911 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 2912 + "dev": true, 2913 + "license": "MIT", 2914 + "engines": { 2915 + "node": ">= 4" 2916 + } 2917 + }, 2918 + "node_modules/import-fresh": { 2919 + "version": "3.3.1", 2920 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 2921 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 2922 + "dev": true, 2923 + "license": "MIT", 2924 + "dependencies": { 2925 + "parent-module": "^1.0.0", 2926 + "resolve-from": "^4.0.0" 2927 + }, 2928 + "engines": { 2929 + "node": ">=6" 2930 + }, 2931 + "funding": { 2932 + "url": "https://github.com/sponsors/sindresorhus" 2933 + } 2934 + }, 2935 + "node_modules/imurmurhash": { 2936 + "version": "0.1.4", 2937 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 2938 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 2939 + "dev": true, 2940 + "license": "MIT", 2941 + "engines": { 2942 + "node": ">=0.8.19" 2943 + } 2944 + }, 2945 + "node_modules/internal-slot": { 2946 + "version": "1.1.0", 2947 + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", 2948 + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", 2949 + "dev": true, 2950 + "license": "MIT", 2951 + "dependencies": { 2952 + "es-errors": "^1.3.0", 2953 + "hasown": "^2.0.2", 2954 + "side-channel": "^1.1.0" 2955 + }, 2956 + "engines": { 2957 + "node": ">= 0.4" 2958 + } 2959 + }, 2960 + "node_modules/is-array-buffer": { 2961 + "version": "3.0.5", 2962 + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", 2963 + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", 2964 + "dev": true, 2965 + "license": "MIT", 2966 + "dependencies": { 2967 + "call-bind": "^1.0.8", 2968 + "call-bound": "^1.0.3", 2969 + "get-intrinsic": "^1.2.6" 2970 + }, 2971 + "engines": { 2972 + "node": ">= 0.4" 2973 + }, 2974 + "funding": { 2975 + "url": "https://github.com/sponsors/ljharb" 2976 + } 2977 + }, 2978 + "node_modules/is-async-function": { 2979 + "version": "2.1.1", 2980 + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", 2981 + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", 2982 + "dev": true, 2983 + "license": "MIT", 2984 + "dependencies": { 2985 + "async-function": "^1.0.0", 2986 + "call-bound": "^1.0.3", 2987 + "get-proto": "^1.0.1", 2988 + "has-tostringtag": "^1.0.2", 2989 + "safe-regex-test": "^1.1.0" 2990 + }, 2991 + "engines": { 2992 + "node": ">= 0.4" 2993 + }, 2994 + "funding": { 2995 + "url": "https://github.com/sponsors/ljharb" 2996 + } 2997 + }, 2998 + "node_modules/is-bigint": { 2999 + "version": "1.1.0", 3000 + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", 3001 + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", 3002 + "dev": true, 3003 + "license": "MIT", 3004 + "dependencies": { 3005 + "has-bigints": "^1.0.2" 3006 + }, 3007 + "engines": { 3008 + "node": ">= 0.4" 3009 + }, 3010 + "funding": { 3011 + "url": "https://github.com/sponsors/ljharb" 3012 + } 3013 + }, 3014 + "node_modules/is-boolean-object": { 3015 + "version": "1.2.2", 3016 + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", 3017 + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", 3018 + "dev": true, 3019 + "license": "MIT", 3020 + "dependencies": { 3021 + "call-bound": "^1.0.3", 3022 + "has-tostringtag": "^1.0.2" 3023 + }, 3024 + "engines": { 3025 + "node": ">= 0.4" 3026 + }, 3027 + "funding": { 3028 + "url": "https://github.com/sponsors/ljharb" 3029 + } 3030 + }, 3031 + "node_modules/is-callable": { 3032 + "version": "1.2.7", 3033 + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 3034 + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", 3035 + "dev": true, 3036 + "license": "MIT", 3037 + "engines": { 3038 + "node": ">= 0.4" 3039 + }, 3040 + "funding": { 3041 + "url": "https://github.com/sponsors/ljharb" 3042 + } 3043 + }, 3044 + "node_modules/is-core-module": { 3045 + "version": "2.16.1", 3046 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", 3047 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", 3048 + "dev": true, 3049 + "license": "MIT", 3050 + "dependencies": { 3051 + "hasown": "^2.0.2" 3052 + }, 3053 + "engines": { 3054 + "node": ">= 0.4" 3055 + }, 3056 + "funding": { 3057 + "url": "https://github.com/sponsors/ljharb" 3058 + } 3059 + }, 3060 + "node_modules/is-data-view": { 3061 + "version": "1.0.2", 3062 + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", 3063 + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", 3064 + "dev": true, 3065 + "license": "MIT", 3066 + "dependencies": { 3067 + "call-bound": "^1.0.2", 3068 + "get-intrinsic": "^1.2.6", 3069 + "is-typed-array": "^1.1.13" 3070 + }, 3071 + "engines": { 3072 + "node": ">= 0.4" 3073 + }, 3074 + "funding": { 3075 + "url": "https://github.com/sponsors/ljharb" 3076 + } 3077 + }, 3078 + "node_modules/is-date-object": { 3079 + "version": "1.1.0", 3080 + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", 3081 + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", 3082 + "dev": true, 3083 + "license": "MIT", 3084 + "dependencies": { 3085 + "call-bound": "^1.0.2", 3086 + "has-tostringtag": "^1.0.2" 3087 + }, 3088 + "engines": { 3089 + "node": ">= 0.4" 3090 + }, 3091 + "funding": { 3092 + "url": "https://github.com/sponsors/ljharb" 3093 + } 3094 + }, 3095 + "node_modules/is-extglob": { 3096 + "version": "2.1.1", 3097 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 3098 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 3099 + "dev": true, 3100 + "license": "MIT", 3101 + "engines": { 3102 + "node": ">=0.10.0" 3103 + } 3104 + }, 3105 + "node_modules/is-finalizationregistry": { 3106 + "version": "1.1.1", 3107 + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", 3108 + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", 3109 + "dev": true, 3110 + "license": "MIT", 3111 + "dependencies": { 3112 + "call-bound": "^1.0.3" 3113 + }, 3114 + "engines": { 3115 + "node": ">= 0.4" 3116 + }, 3117 + "funding": { 3118 + "url": "https://github.com/sponsors/ljharb" 3119 + } 3120 + }, 3121 + "node_modules/is-generator-function": { 3122 + "version": "1.1.2", 3123 + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", 3124 + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", 3125 + "dev": true, 3126 + "license": "MIT", 3127 + "dependencies": { 3128 + "call-bound": "^1.0.4", 3129 + "generator-function": "^2.0.0", 3130 + "get-proto": "^1.0.1", 3131 + "has-tostringtag": "^1.0.2", 3132 + "safe-regex-test": "^1.1.0" 3133 + }, 3134 + "engines": { 3135 + "node": ">= 0.4" 3136 + }, 3137 + "funding": { 3138 + "url": "https://github.com/sponsors/ljharb" 3139 + } 3140 + }, 3141 + "node_modules/is-glob": { 3142 + "version": "4.0.3", 3143 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 3144 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 3145 + "dev": true, 3146 + "license": "MIT", 3147 + "dependencies": { 3148 + "is-extglob": "^2.1.1" 3149 + }, 3150 + "engines": { 3151 + "node": ">=0.10.0" 3152 + } 3153 + }, 3154 + "node_modules/is-map": { 3155 + "version": "2.0.3", 3156 + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", 3157 + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", 3158 + "dev": true, 3159 + "license": "MIT", 3160 + "engines": { 3161 + "node": ">= 0.4" 3162 + }, 3163 + "funding": { 3164 + "url": "https://github.com/sponsors/ljharb" 3165 + } 3166 + }, 3167 + "node_modules/is-negative-zero": { 3168 + "version": "2.0.3", 3169 + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", 3170 + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", 3171 + "dev": true, 3172 + "license": "MIT", 3173 + "engines": { 3174 + "node": ">= 0.4" 3175 + }, 3176 + "funding": { 3177 + "url": "https://github.com/sponsors/ljharb" 3178 + } 3179 + }, 3180 + "node_modules/is-number-object": { 3181 + "version": "1.1.1", 3182 + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", 3183 + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", 3184 + "dev": true, 3185 + "license": "MIT", 3186 + "dependencies": { 3187 + "call-bound": "^1.0.3", 3188 + "has-tostringtag": "^1.0.2" 3189 + }, 3190 + "engines": { 3191 + "node": ">= 0.4" 3192 + }, 3193 + "funding": { 3194 + "url": "https://github.com/sponsors/ljharb" 3195 + } 3196 + }, 3197 + "node_modules/is-regex": { 3198 + "version": "1.2.1", 3199 + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", 3200 + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", 3201 + "dev": true, 3202 + "license": "MIT", 3203 + "dependencies": { 3204 + "call-bound": "^1.0.2", 3205 + "gopd": "^1.2.0", 3206 + "has-tostringtag": "^1.0.2", 3207 + "hasown": "^2.0.2" 3208 + }, 3209 + "engines": { 3210 + "node": ">= 0.4" 3211 + }, 3212 + "funding": { 3213 + "url": "https://github.com/sponsors/ljharb" 3214 + } 3215 + }, 3216 + "node_modules/is-set": { 3217 + "version": "2.0.3", 3218 + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", 3219 + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", 3220 + "dev": true, 3221 + "license": "MIT", 3222 + "engines": { 3223 + "node": ">= 0.4" 3224 + }, 3225 + "funding": { 3226 + "url": "https://github.com/sponsors/ljharb" 3227 + } 3228 + }, 3229 + "node_modules/is-shared-array-buffer": { 3230 + "version": "1.0.4", 3231 + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", 3232 + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", 3233 + "dev": true, 3234 + "license": "MIT", 3235 + "dependencies": { 3236 + "call-bound": "^1.0.3" 3237 + }, 3238 + "engines": { 3239 + "node": ">= 0.4" 3240 + }, 3241 + "funding": { 3242 + "url": "https://github.com/sponsors/ljharb" 3243 + } 3244 + }, 3245 + "node_modules/is-string": { 3246 + "version": "1.1.1", 3247 + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", 3248 + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", 3249 + "dev": true, 3250 + "license": "MIT", 3251 + "dependencies": { 3252 + "call-bound": "^1.0.3", 3253 + "has-tostringtag": "^1.0.2" 3254 + }, 3255 + "engines": { 3256 + "node": ">= 0.4" 3257 + }, 3258 + "funding": { 3259 + "url": "https://github.com/sponsors/ljharb" 3260 + } 3261 + }, 3262 + "node_modules/is-symbol": { 3263 + "version": "1.1.1", 3264 + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", 3265 + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", 3266 + "dev": true, 3267 + "license": "MIT", 3268 + "dependencies": { 3269 + "call-bound": "^1.0.2", 3270 + "has-symbols": "^1.1.0", 3271 + "safe-regex-test": "^1.1.0" 3272 + }, 3273 + "engines": { 3274 + "node": ">= 0.4" 3275 + }, 3276 + "funding": { 3277 + "url": "https://github.com/sponsors/ljharb" 3278 + } 3279 + }, 3280 + "node_modules/is-typed-array": { 3281 + "version": "1.1.15", 3282 + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", 3283 + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", 3284 + "dev": true, 3285 + "license": "MIT", 3286 + "dependencies": { 3287 + "which-typed-array": "^1.1.16" 3288 + }, 3289 + "engines": { 3290 + "node": ">= 0.4" 3291 + }, 3292 + "funding": { 3293 + "url": "https://github.com/sponsors/ljharb" 3294 + } 3295 + }, 3296 + "node_modules/is-weakmap": { 3297 + "version": "2.0.2", 3298 + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", 3299 + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", 3300 + "dev": true, 3301 + "license": "MIT", 3302 + "engines": { 3303 + "node": ">= 0.4" 3304 + }, 3305 + "funding": { 3306 + "url": "https://github.com/sponsors/ljharb" 3307 + } 3308 + }, 3309 + "node_modules/is-weakref": { 3310 + "version": "1.1.1", 3311 + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", 3312 + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", 3313 + "dev": true, 3314 + "license": "MIT", 3315 + "dependencies": { 3316 + "call-bound": "^1.0.3" 3317 + }, 3318 + "engines": { 3319 + "node": ">= 0.4" 3320 + }, 3321 + "funding": { 3322 + "url": "https://github.com/sponsors/ljharb" 3323 + } 3324 + }, 3325 + "node_modules/is-weakset": { 3326 + "version": "2.0.4", 3327 + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", 3328 + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", 3329 + "dev": true, 3330 + "license": "MIT", 3331 + "dependencies": { 3332 + "call-bound": "^1.0.3", 3333 + "get-intrinsic": "^1.2.6" 3334 + }, 3335 + "engines": { 3336 + "node": ">= 0.4" 3337 + }, 3338 + "funding": { 3339 + "url": "https://github.com/sponsors/ljharb" 3340 + } 3341 + }, 3342 + "node_modules/isarray": { 3343 + "version": "2.0.5", 3344 + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", 3345 + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", 3346 + "dev": true, 3347 + "license": "MIT" 3348 + }, 3349 + "node_modules/isexe": { 3350 + "version": "2.0.0", 3351 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 3352 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 3353 + "dev": true, 3354 + "license": "ISC" 3355 + }, 3356 + "node_modules/iterator.prototype": { 3357 + "version": "1.1.5", 3358 + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", 3359 + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", 3360 + "dev": true, 3361 + "license": "MIT", 3362 + "dependencies": { 3363 + "define-data-property": "^1.1.4", 3364 + "es-object-atoms": "^1.0.0", 3365 + "get-intrinsic": "^1.2.6", 3366 + "get-proto": "^1.0.0", 3367 + "has-symbols": "^1.1.0", 3368 + "set-function-name": "^2.0.2" 3369 + }, 3370 + "engines": { 3371 + "node": ">= 0.4" 3372 + } 3373 + }, 1425 3374 "node_modules/js-tokens": { 1426 3375 "version": "4.0.0", 1427 3376 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1428 3377 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 1429 3378 "license": "MIT" 1430 3379 }, 3380 + "node_modules/js-yaml": { 3381 + "version": "4.1.1", 3382 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 3383 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 3384 + "dev": true, 3385 + "license": "MIT", 3386 + "dependencies": { 3387 + "argparse": "^2.0.1" 3388 + }, 3389 + "bin": { 3390 + "js-yaml": "bin/js-yaml.js" 3391 + } 3392 + }, 1431 3393 "node_modules/jsesc": { 1432 3394 "version": "3.1.0", 1433 3395 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", ··· 1441 3403 "node": ">=6" 1442 3404 } 1443 3405 }, 3406 + "node_modules/json-buffer": { 3407 + "version": "3.0.1", 3408 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 3409 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 3410 + "dev": true, 3411 + "license": "MIT" 3412 + }, 3413 + "node_modules/json-schema-traverse": { 3414 + "version": "0.4.1", 3415 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 3416 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 3417 + "dev": true, 3418 + "license": "MIT" 3419 + }, 3420 + "node_modules/json-stable-stringify-without-jsonify": { 3421 + "version": "1.0.1", 3422 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 3423 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 3424 + "dev": true, 3425 + "license": "MIT" 3426 + }, 1444 3427 "node_modules/json5": { 1445 3428 "version": "2.2.3", 1446 3429 "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", ··· 1454 3437 "node": ">=6" 1455 3438 } 1456 3439 }, 3440 + "node_modules/jsx-ast-utils": { 3441 + "version": "3.3.5", 3442 + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", 3443 + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", 3444 + "dev": true, 3445 + "license": "MIT", 3446 + "dependencies": { 3447 + "array-includes": "^3.1.6", 3448 + "array.prototype.flat": "^1.3.1", 3449 + "object.assign": "^4.1.4", 3450 + "object.values": "^1.1.6" 3451 + }, 3452 + "engines": { 3453 + "node": ">=4.0" 3454 + } 3455 + }, 3456 + "node_modules/keyv": { 3457 + "version": "4.5.4", 3458 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 3459 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 3460 + "dev": true, 3461 + "license": "MIT", 3462 + "dependencies": { 3463 + "json-buffer": "3.0.1" 3464 + } 3465 + }, 3466 + "node_modules/levn": { 3467 + "version": "0.4.1", 3468 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 3469 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 3470 + "dev": true, 3471 + "license": "MIT", 3472 + "dependencies": { 3473 + "prelude-ls": "^1.2.1", 3474 + "type-check": "~0.4.0" 3475 + }, 3476 + "engines": { 3477 + "node": ">= 0.8.0" 3478 + } 3479 + }, 3480 + "node_modules/locate-path": { 3481 + "version": "6.0.0", 3482 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 3483 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 3484 + "dev": true, 3485 + "license": "MIT", 3486 + "dependencies": { 3487 + "p-locate": "^5.0.0" 3488 + }, 3489 + "engines": { 3490 + "node": ">=10" 3491 + }, 3492 + "funding": { 3493 + "url": "https://github.com/sponsors/sindresorhus" 3494 + } 3495 + }, 3496 + "node_modules/lodash.merge": { 3497 + "version": "4.6.2", 3498 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 3499 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 3500 + "dev": true, 3501 + "license": "MIT" 3502 + }, 1457 3503 "node_modules/loose-envify": { 1458 3504 "version": "1.4.0", 1459 3505 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", ··· 1485 3531 "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" 1486 3532 } 1487 3533 }, 3534 + "node_modules/math-intrinsics": { 3535 + "version": "1.1.0", 3536 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 3537 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 3538 + "dev": true, 3539 + "license": "MIT", 3540 + "engines": { 3541 + "node": ">= 0.4" 3542 + } 3543 + }, 3544 + "node_modules/minimatch": { 3545 + "version": "3.1.2", 3546 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 3547 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 3548 + "dev": true, 3549 + "license": "ISC", 3550 + "dependencies": { 3551 + "brace-expansion": "^1.1.7" 3552 + }, 3553 + "engines": { 3554 + "node": "*" 3555 + } 3556 + }, 1488 3557 "node_modules/ms": { 1489 3558 "version": "2.1.3", 1490 3559 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 1511 3580 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1512 3581 } 1513 3582 }, 3583 + "node_modules/natural-compare": { 3584 + "version": "1.4.0", 3585 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 3586 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 3587 + "dev": true, 3588 + "license": "MIT" 3589 + }, 1514 3590 "node_modules/node-releases": { 1515 3591 "version": "2.0.27", 1516 3592 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", 1517 3593 "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", 3594 + "dev": true, 3595 + "license": "MIT" 3596 + }, 3597 + "node_modules/object-assign": { 3598 + "version": "4.1.1", 3599 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 3600 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 3601 + "dev": true, 3602 + "license": "MIT", 3603 + "engines": { 3604 + "node": ">=0.10.0" 3605 + } 3606 + }, 3607 + "node_modules/object-inspect": { 3608 + "version": "1.13.4", 3609 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 3610 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 3611 + "dev": true, 3612 + "license": "MIT", 3613 + "engines": { 3614 + "node": ">= 0.4" 3615 + }, 3616 + "funding": { 3617 + "url": "https://github.com/sponsors/ljharb" 3618 + } 3619 + }, 3620 + "node_modules/object-keys": { 3621 + "version": "1.1.1", 3622 + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 3623 + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 3624 + "dev": true, 3625 + "license": "MIT", 3626 + "engines": { 3627 + "node": ">= 0.4" 3628 + } 3629 + }, 3630 + "node_modules/object.assign": { 3631 + "version": "4.1.7", 3632 + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", 3633 + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", 3634 + "dev": true, 3635 + "license": "MIT", 3636 + "dependencies": { 3637 + "call-bind": "^1.0.8", 3638 + "call-bound": "^1.0.3", 3639 + "define-properties": "^1.2.1", 3640 + "es-object-atoms": "^1.0.0", 3641 + "has-symbols": "^1.1.0", 3642 + "object-keys": "^1.1.1" 3643 + }, 3644 + "engines": { 3645 + "node": ">= 0.4" 3646 + }, 3647 + "funding": { 3648 + "url": "https://github.com/sponsors/ljharb" 3649 + } 3650 + }, 3651 + "node_modules/object.entries": { 3652 + "version": "1.1.9", 3653 + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", 3654 + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", 3655 + "dev": true, 3656 + "license": "MIT", 3657 + "dependencies": { 3658 + "call-bind": "^1.0.8", 3659 + "call-bound": "^1.0.4", 3660 + "define-properties": "^1.2.1", 3661 + "es-object-atoms": "^1.1.1" 3662 + }, 3663 + "engines": { 3664 + "node": ">= 0.4" 3665 + } 3666 + }, 3667 + "node_modules/object.fromentries": { 3668 + "version": "2.0.8", 3669 + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", 3670 + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", 3671 + "dev": true, 3672 + "license": "MIT", 3673 + "dependencies": { 3674 + "call-bind": "^1.0.7", 3675 + "define-properties": "^1.2.1", 3676 + "es-abstract": "^1.23.2", 3677 + "es-object-atoms": "^1.0.0" 3678 + }, 3679 + "engines": { 3680 + "node": ">= 0.4" 3681 + }, 3682 + "funding": { 3683 + "url": "https://github.com/sponsors/ljharb" 3684 + } 3685 + }, 3686 + "node_modules/object.values": { 3687 + "version": "1.2.1", 3688 + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", 3689 + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", 3690 + "dev": true, 3691 + "license": "MIT", 3692 + "dependencies": { 3693 + "call-bind": "^1.0.8", 3694 + "call-bound": "^1.0.3", 3695 + "define-properties": "^1.2.1", 3696 + "es-object-atoms": "^1.0.0" 3697 + }, 3698 + "engines": { 3699 + "node": ">= 0.4" 3700 + }, 3701 + "funding": { 3702 + "url": "https://github.com/sponsors/ljharb" 3703 + } 3704 + }, 3705 + "node_modules/optionator": { 3706 + "version": "0.9.4", 3707 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 3708 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 3709 + "dev": true, 3710 + "license": "MIT", 3711 + "dependencies": { 3712 + "deep-is": "^0.1.3", 3713 + "fast-levenshtein": "^2.0.6", 3714 + "levn": "^0.4.1", 3715 + "prelude-ls": "^1.2.1", 3716 + "type-check": "^0.4.0", 3717 + "word-wrap": "^1.2.5" 3718 + }, 3719 + "engines": { 3720 + "node": ">= 0.8.0" 3721 + } 3722 + }, 3723 + "node_modules/own-keys": { 3724 + "version": "1.0.1", 3725 + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", 3726 + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", 3727 + "dev": true, 3728 + "license": "MIT", 3729 + "dependencies": { 3730 + "get-intrinsic": "^1.2.6", 3731 + "object-keys": "^1.1.1", 3732 + "safe-push-apply": "^1.0.0" 3733 + }, 3734 + "engines": { 3735 + "node": ">= 0.4" 3736 + }, 3737 + "funding": { 3738 + "url": "https://github.com/sponsors/ljharb" 3739 + } 3740 + }, 3741 + "node_modules/p-limit": { 3742 + "version": "3.1.0", 3743 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 3744 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 3745 + "dev": true, 3746 + "license": "MIT", 3747 + "dependencies": { 3748 + "yocto-queue": "^0.1.0" 3749 + }, 3750 + "engines": { 3751 + "node": ">=10" 3752 + }, 3753 + "funding": { 3754 + "url": "https://github.com/sponsors/sindresorhus" 3755 + } 3756 + }, 3757 + "node_modules/p-locate": { 3758 + "version": "5.0.0", 3759 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 3760 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 3761 + "dev": true, 3762 + "license": "MIT", 3763 + "dependencies": { 3764 + "p-limit": "^3.0.2" 3765 + }, 3766 + "engines": { 3767 + "node": ">=10" 3768 + }, 3769 + "funding": { 3770 + "url": "https://github.com/sponsors/sindresorhus" 3771 + } 3772 + }, 3773 + "node_modules/parent-module": { 3774 + "version": "1.0.1", 3775 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 3776 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 3777 + "dev": true, 3778 + "license": "MIT", 3779 + "dependencies": { 3780 + "callsites": "^3.0.0" 3781 + }, 3782 + "engines": { 3783 + "node": ">=6" 3784 + } 3785 + }, 3786 + "node_modules/path-exists": { 3787 + "version": "4.0.0", 3788 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 3789 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 3790 + "dev": true, 3791 + "license": "MIT", 3792 + "engines": { 3793 + "node": ">=8" 3794 + } 3795 + }, 3796 + "node_modules/path-key": { 3797 + "version": "3.1.1", 3798 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 3799 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 3800 + "dev": true, 3801 + "license": "MIT", 3802 + "engines": { 3803 + "node": ">=8" 3804 + } 3805 + }, 3806 + "node_modules/path-parse": { 3807 + "version": "1.0.7", 3808 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 3809 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1518 3810 "dev": true, 1519 3811 "license": "MIT" 1520 3812 }, ··· 1539 3831 "url": "https://github.com/sponsors/jonschlinkert" 1540 3832 } 1541 3833 }, 3834 + "node_modules/possible-typed-array-names": { 3835 + "version": "1.1.0", 3836 + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", 3837 + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", 3838 + "dev": true, 3839 + "license": "MIT", 3840 + "engines": { 3841 + "node": ">= 0.4" 3842 + } 3843 + }, 1542 3844 "node_modules/postcss": { 1543 3845 "version": "8.5.6", 1544 3846 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", ··· 1568 3870 "node": "^10 || ^12 || >=14" 1569 3871 } 1570 3872 }, 3873 + "node_modules/prelude-ls": { 3874 + "version": "1.2.1", 3875 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 3876 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 3877 + "dev": true, 3878 + "license": "MIT", 3879 + "engines": { 3880 + "node": ">= 0.8.0" 3881 + } 3882 + }, 3883 + "node_modules/prop-types": { 3884 + "version": "15.8.1", 3885 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 3886 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 3887 + "dev": true, 3888 + "license": "MIT", 3889 + "dependencies": { 3890 + "loose-envify": "^1.4.0", 3891 + "object-assign": "^4.1.1", 3892 + "react-is": "^16.13.1" 3893 + } 3894 + }, 3895 + "node_modules/punycode": { 3896 + "version": "2.3.1", 3897 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 3898 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 3899 + "dev": true, 3900 + "license": "MIT", 3901 + "engines": { 3902 + "node": ">=6" 3903 + } 3904 + }, 1571 3905 "node_modules/react": { 1572 3906 "version": "18.3.1", 1573 3907 "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", ··· 1604 3938 "react": "*" 1605 3939 } 1606 3940 }, 3941 + "node_modules/react-is": { 3942 + "version": "16.13.1", 3943 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 3944 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 3945 + "dev": true, 3946 + "license": "MIT" 3947 + }, 1607 3948 "node_modules/react-refresh": { 1608 3949 "version": "0.17.0", 1609 3950 "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", ··· 1615 3956 } 1616 3957 }, 1617 3958 "node_modules/react-router": { 1618 - "version": "6.30.2", 1619 - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", 1620 - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", 3959 + "version": "6.30.3", 3960 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", 3961 + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", 1621 3962 "license": "MIT", 1622 3963 "dependencies": { 1623 - "@remix-run/router": "1.23.1" 3964 + "@remix-run/router": "1.23.2" 1624 3965 }, 1625 3966 "engines": { 1626 3967 "node": ">=14.0.0" ··· 1630 3971 } 1631 3972 }, 1632 3973 "node_modules/react-router-dom": { 1633 - "version": "6.30.2", 1634 - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", 1635 - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", 3974 + "version": "6.30.3", 3975 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", 3976 + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", 1636 3977 "license": "MIT", 1637 3978 "dependencies": { 1638 - "@remix-run/router": "1.23.1", 1639 - "react-router": "6.30.2" 3979 + "@remix-run/router": "1.23.2", 3980 + "react-router": "6.30.3" 1640 3981 }, 1641 3982 "engines": { 1642 3983 "node": ">=14.0.0" ··· 1646 3987 "react-dom": ">=16.8" 1647 3988 } 1648 3989 }, 3990 + "node_modules/reflect.getprototypeof": { 3991 + "version": "1.0.10", 3992 + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", 3993 + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", 3994 + "dev": true, 3995 + "license": "MIT", 3996 + "dependencies": { 3997 + "call-bind": "^1.0.8", 3998 + "define-properties": "^1.2.1", 3999 + "es-abstract": "^1.23.9", 4000 + "es-errors": "^1.3.0", 4001 + "es-object-atoms": "^1.0.0", 4002 + "get-intrinsic": "^1.2.7", 4003 + "get-proto": "^1.0.1", 4004 + "which-builtin-type": "^1.2.1" 4005 + }, 4006 + "engines": { 4007 + "node": ">= 0.4" 4008 + }, 4009 + "funding": { 4010 + "url": "https://github.com/sponsors/ljharb" 4011 + } 4012 + }, 4013 + "node_modules/regexp.prototype.flags": { 4014 + "version": "1.5.4", 4015 + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", 4016 + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", 4017 + "dev": true, 4018 + "license": "MIT", 4019 + "dependencies": { 4020 + "call-bind": "^1.0.8", 4021 + "define-properties": "^1.2.1", 4022 + "es-errors": "^1.3.0", 4023 + "get-proto": "^1.0.1", 4024 + "gopd": "^1.2.0", 4025 + "set-function-name": "^2.0.2" 4026 + }, 4027 + "engines": { 4028 + "node": ">= 0.4" 4029 + }, 4030 + "funding": { 4031 + "url": "https://github.com/sponsors/ljharb" 4032 + } 4033 + }, 4034 + "node_modules/resolve": { 4035 + "version": "2.0.0-next.5", 4036 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", 4037 + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", 4038 + "dev": true, 4039 + "license": "MIT", 4040 + "dependencies": { 4041 + "is-core-module": "^2.13.0", 4042 + "path-parse": "^1.0.7", 4043 + "supports-preserve-symlinks-flag": "^1.0.0" 4044 + }, 4045 + "bin": { 4046 + "resolve": "bin/resolve" 4047 + }, 4048 + "funding": { 4049 + "url": "https://github.com/sponsors/ljharb" 4050 + } 4051 + }, 4052 + "node_modules/resolve-from": { 4053 + "version": "4.0.0", 4054 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 4055 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 4056 + "dev": true, 4057 + "license": "MIT", 4058 + "engines": { 4059 + "node": ">=4" 4060 + } 4061 + }, 1649 4062 "node_modules/rollup": { 1650 4063 "version": "4.54.0", 1651 4064 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", ··· 1688 4101 "fsevents": "~2.3.2" 1689 4102 } 1690 4103 }, 4104 + "node_modules/safe-array-concat": { 4105 + "version": "1.1.3", 4106 + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", 4107 + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", 4108 + "dev": true, 4109 + "license": "MIT", 4110 + "dependencies": { 4111 + "call-bind": "^1.0.8", 4112 + "call-bound": "^1.0.2", 4113 + "get-intrinsic": "^1.2.6", 4114 + "has-symbols": "^1.1.0", 4115 + "isarray": "^2.0.5" 4116 + }, 4117 + "engines": { 4118 + "node": ">=0.4" 4119 + }, 4120 + "funding": { 4121 + "url": "https://github.com/sponsors/ljharb" 4122 + } 4123 + }, 4124 + "node_modules/safe-push-apply": { 4125 + "version": "1.0.0", 4126 + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", 4127 + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", 4128 + "dev": true, 4129 + "license": "MIT", 4130 + "dependencies": { 4131 + "es-errors": "^1.3.0", 4132 + "isarray": "^2.0.5" 4133 + }, 4134 + "engines": { 4135 + "node": ">= 0.4" 4136 + }, 4137 + "funding": { 4138 + "url": "https://github.com/sponsors/ljharb" 4139 + } 4140 + }, 4141 + "node_modules/safe-regex-test": { 4142 + "version": "1.1.0", 4143 + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", 4144 + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", 4145 + "dev": true, 4146 + "license": "MIT", 4147 + "dependencies": { 4148 + "call-bound": "^1.0.2", 4149 + "es-errors": "^1.3.0", 4150 + "is-regex": "^1.2.1" 4151 + }, 4152 + "engines": { 4153 + "node": ">= 0.4" 4154 + }, 4155 + "funding": { 4156 + "url": "https://github.com/sponsors/ljharb" 4157 + } 4158 + }, 1691 4159 "node_modules/scheduler": { 1692 4160 "version": "0.23.2", 1693 4161 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", ··· 1707 4175 "semver": "bin/semver.js" 1708 4176 } 1709 4177 }, 4178 + "node_modules/set-function-length": { 4179 + "version": "1.2.2", 4180 + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 4181 + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 4182 + "dev": true, 4183 + "license": "MIT", 4184 + "dependencies": { 4185 + "define-data-property": "^1.1.4", 4186 + "es-errors": "^1.3.0", 4187 + "function-bind": "^1.1.2", 4188 + "get-intrinsic": "^1.2.4", 4189 + "gopd": "^1.0.1", 4190 + "has-property-descriptors": "^1.0.2" 4191 + }, 4192 + "engines": { 4193 + "node": ">= 0.4" 4194 + } 4195 + }, 4196 + "node_modules/set-function-name": { 4197 + "version": "2.0.2", 4198 + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", 4199 + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", 4200 + "dev": true, 4201 + "license": "MIT", 4202 + "dependencies": { 4203 + "define-data-property": "^1.1.4", 4204 + "es-errors": "^1.3.0", 4205 + "functions-have-names": "^1.2.3", 4206 + "has-property-descriptors": "^1.0.2" 4207 + }, 4208 + "engines": { 4209 + "node": ">= 0.4" 4210 + } 4211 + }, 4212 + "node_modules/set-proto": { 4213 + "version": "1.0.0", 4214 + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", 4215 + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", 4216 + "dev": true, 4217 + "license": "MIT", 4218 + "dependencies": { 4219 + "dunder-proto": "^1.0.1", 4220 + "es-errors": "^1.3.0", 4221 + "es-object-atoms": "^1.0.0" 4222 + }, 4223 + "engines": { 4224 + "node": ">= 0.4" 4225 + } 4226 + }, 4227 + "node_modules/shebang-command": { 4228 + "version": "2.0.0", 4229 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 4230 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 4231 + "dev": true, 4232 + "license": "MIT", 4233 + "dependencies": { 4234 + "shebang-regex": "^3.0.0" 4235 + }, 4236 + "engines": { 4237 + "node": ">=8" 4238 + } 4239 + }, 4240 + "node_modules/shebang-regex": { 4241 + "version": "3.0.0", 4242 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 4243 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 4244 + "dev": true, 4245 + "license": "MIT", 4246 + "engines": { 4247 + "node": ">=8" 4248 + } 4249 + }, 4250 + "node_modules/side-channel": { 4251 + "version": "1.1.0", 4252 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 4253 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 4254 + "dev": true, 4255 + "license": "MIT", 4256 + "dependencies": { 4257 + "es-errors": "^1.3.0", 4258 + "object-inspect": "^1.13.3", 4259 + "side-channel-list": "^1.0.0", 4260 + "side-channel-map": "^1.0.1", 4261 + "side-channel-weakmap": "^1.0.2" 4262 + }, 4263 + "engines": { 4264 + "node": ">= 0.4" 4265 + }, 4266 + "funding": { 4267 + "url": "https://github.com/sponsors/ljharb" 4268 + } 4269 + }, 4270 + "node_modules/side-channel-list": { 4271 + "version": "1.0.0", 4272 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 4273 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 4274 + "dev": true, 4275 + "license": "MIT", 4276 + "dependencies": { 4277 + "es-errors": "^1.3.0", 4278 + "object-inspect": "^1.13.3" 4279 + }, 4280 + "engines": { 4281 + "node": ">= 0.4" 4282 + }, 4283 + "funding": { 4284 + "url": "https://github.com/sponsors/ljharb" 4285 + } 4286 + }, 4287 + "node_modules/side-channel-map": { 4288 + "version": "1.0.1", 4289 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 4290 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 4291 + "dev": true, 4292 + "license": "MIT", 4293 + "dependencies": { 4294 + "call-bound": "^1.0.2", 4295 + "es-errors": "^1.3.0", 4296 + "get-intrinsic": "^1.2.5", 4297 + "object-inspect": "^1.13.3" 4298 + }, 4299 + "engines": { 4300 + "node": ">= 0.4" 4301 + }, 4302 + "funding": { 4303 + "url": "https://github.com/sponsors/ljharb" 4304 + } 4305 + }, 4306 + "node_modules/side-channel-weakmap": { 4307 + "version": "1.0.2", 4308 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 4309 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 4310 + "dev": true, 4311 + "license": "MIT", 4312 + "dependencies": { 4313 + "call-bound": "^1.0.2", 4314 + "es-errors": "^1.3.0", 4315 + "get-intrinsic": "^1.2.5", 4316 + "object-inspect": "^1.13.3", 4317 + "side-channel-map": "^1.0.1" 4318 + }, 4319 + "engines": { 4320 + "node": ">= 0.4" 4321 + }, 4322 + "funding": { 4323 + "url": "https://github.com/sponsors/ljharb" 4324 + } 4325 + }, 1710 4326 "node_modules/source-map-js": { 1711 4327 "version": "1.2.1", 1712 4328 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", ··· 1717 4333 "node": ">=0.10.0" 1718 4334 } 1719 4335 }, 4336 + "node_modules/stop-iteration-iterator": { 4337 + "version": "1.1.0", 4338 + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", 4339 + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", 4340 + "dev": true, 4341 + "license": "MIT", 4342 + "dependencies": { 4343 + "es-errors": "^1.3.0", 4344 + "internal-slot": "^1.1.0" 4345 + }, 4346 + "engines": { 4347 + "node": ">= 0.4" 4348 + } 4349 + }, 4350 + "node_modules/string.prototype.matchall": { 4351 + "version": "4.0.12", 4352 + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", 4353 + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", 4354 + "dev": true, 4355 + "license": "MIT", 4356 + "dependencies": { 4357 + "call-bind": "^1.0.8", 4358 + "call-bound": "^1.0.3", 4359 + "define-properties": "^1.2.1", 4360 + "es-abstract": "^1.23.6", 4361 + "es-errors": "^1.3.0", 4362 + "es-object-atoms": "^1.0.0", 4363 + "get-intrinsic": "^1.2.6", 4364 + "gopd": "^1.2.0", 4365 + "has-symbols": "^1.1.0", 4366 + "internal-slot": "^1.1.0", 4367 + "regexp.prototype.flags": "^1.5.3", 4368 + "set-function-name": "^2.0.2", 4369 + "side-channel": "^1.1.0" 4370 + }, 4371 + "engines": { 4372 + "node": ">= 0.4" 4373 + }, 4374 + "funding": { 4375 + "url": "https://github.com/sponsors/ljharb" 4376 + } 4377 + }, 4378 + "node_modules/string.prototype.repeat": { 4379 + "version": "1.0.0", 4380 + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", 4381 + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", 4382 + "dev": true, 4383 + "license": "MIT", 4384 + "dependencies": { 4385 + "define-properties": "^1.1.3", 4386 + "es-abstract": "^1.17.5" 4387 + } 4388 + }, 4389 + "node_modules/string.prototype.trim": { 4390 + "version": "1.2.10", 4391 + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", 4392 + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", 4393 + "dev": true, 4394 + "license": "MIT", 4395 + "dependencies": { 4396 + "call-bind": "^1.0.8", 4397 + "call-bound": "^1.0.2", 4398 + "define-data-property": "^1.1.4", 4399 + "define-properties": "^1.2.1", 4400 + "es-abstract": "^1.23.5", 4401 + "es-object-atoms": "^1.0.0", 4402 + "has-property-descriptors": "^1.0.2" 4403 + }, 4404 + "engines": { 4405 + "node": ">= 0.4" 4406 + }, 4407 + "funding": { 4408 + "url": "https://github.com/sponsors/ljharb" 4409 + } 4410 + }, 4411 + "node_modules/string.prototype.trimend": { 4412 + "version": "1.0.9", 4413 + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", 4414 + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", 4415 + "dev": true, 4416 + "license": "MIT", 4417 + "dependencies": { 4418 + "call-bind": "^1.0.8", 4419 + "call-bound": "^1.0.2", 4420 + "define-properties": "^1.2.1", 4421 + "es-object-atoms": "^1.0.0" 4422 + }, 4423 + "engines": { 4424 + "node": ">= 0.4" 4425 + }, 4426 + "funding": { 4427 + "url": "https://github.com/sponsors/ljharb" 4428 + } 4429 + }, 4430 + "node_modules/string.prototype.trimstart": { 4431 + "version": "1.0.8", 4432 + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", 4433 + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", 4434 + "dev": true, 4435 + "license": "MIT", 4436 + "dependencies": { 4437 + "call-bind": "^1.0.7", 4438 + "define-properties": "^1.2.1", 4439 + "es-object-atoms": "^1.0.0" 4440 + }, 4441 + "engines": { 4442 + "node": ">= 0.4" 4443 + }, 4444 + "funding": { 4445 + "url": "https://github.com/sponsors/ljharb" 4446 + } 4447 + }, 4448 + "node_modules/strip-json-comments": { 4449 + "version": "3.1.1", 4450 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 4451 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 4452 + "dev": true, 4453 + "license": "MIT", 4454 + "engines": { 4455 + "node": ">=8" 4456 + }, 4457 + "funding": { 4458 + "url": "https://github.com/sponsors/sindresorhus" 4459 + } 4460 + }, 4461 + "node_modules/supports-color": { 4462 + "version": "7.2.0", 4463 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 4464 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 4465 + "dev": true, 4466 + "license": "MIT", 4467 + "dependencies": { 4468 + "has-flag": "^4.0.0" 4469 + }, 4470 + "engines": { 4471 + "node": ">=8" 4472 + } 4473 + }, 4474 + "node_modules/supports-preserve-symlinks-flag": { 4475 + "version": "1.0.0", 4476 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 4477 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 4478 + "dev": true, 4479 + "license": "MIT", 4480 + "engines": { 4481 + "node": ">= 0.4" 4482 + }, 4483 + "funding": { 4484 + "url": "https://github.com/sponsors/ljharb" 4485 + } 4486 + }, 1720 4487 "node_modules/tinyglobby": { 1721 4488 "version": "0.2.15", 1722 4489 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", ··· 1734 4501 "url": "https://github.com/sponsors/SuperchupuDev" 1735 4502 } 1736 4503 }, 4504 + "node_modules/type-check": { 4505 + "version": "0.4.0", 4506 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 4507 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 4508 + "dev": true, 4509 + "license": "MIT", 4510 + "dependencies": { 4511 + "prelude-ls": "^1.2.1" 4512 + }, 4513 + "engines": { 4514 + "node": ">= 0.8.0" 4515 + } 4516 + }, 4517 + "node_modules/typed-array-buffer": { 4518 + "version": "1.0.3", 4519 + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", 4520 + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", 4521 + "dev": true, 4522 + "license": "MIT", 4523 + "dependencies": { 4524 + "call-bound": "^1.0.3", 4525 + "es-errors": "^1.3.0", 4526 + "is-typed-array": "^1.1.14" 4527 + }, 4528 + "engines": { 4529 + "node": ">= 0.4" 4530 + } 4531 + }, 4532 + "node_modules/typed-array-byte-length": { 4533 + "version": "1.0.3", 4534 + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", 4535 + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", 4536 + "dev": true, 4537 + "license": "MIT", 4538 + "dependencies": { 4539 + "call-bind": "^1.0.8", 4540 + "for-each": "^0.3.3", 4541 + "gopd": "^1.2.0", 4542 + "has-proto": "^1.2.0", 4543 + "is-typed-array": "^1.1.14" 4544 + }, 4545 + "engines": { 4546 + "node": ">= 0.4" 4547 + }, 4548 + "funding": { 4549 + "url": "https://github.com/sponsors/ljharb" 4550 + } 4551 + }, 4552 + "node_modules/typed-array-byte-offset": { 4553 + "version": "1.0.4", 4554 + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", 4555 + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", 4556 + "dev": true, 4557 + "license": "MIT", 4558 + "dependencies": { 4559 + "available-typed-arrays": "^1.0.7", 4560 + "call-bind": "^1.0.8", 4561 + "for-each": "^0.3.3", 4562 + "gopd": "^1.2.0", 4563 + "has-proto": "^1.2.0", 4564 + "is-typed-array": "^1.1.15", 4565 + "reflect.getprototypeof": "^1.0.9" 4566 + }, 4567 + "engines": { 4568 + "node": ">= 0.4" 4569 + }, 4570 + "funding": { 4571 + "url": "https://github.com/sponsors/ljharb" 4572 + } 4573 + }, 4574 + "node_modules/typed-array-length": { 4575 + "version": "1.0.7", 4576 + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", 4577 + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", 4578 + "dev": true, 4579 + "license": "MIT", 4580 + "dependencies": { 4581 + "call-bind": "^1.0.7", 4582 + "for-each": "^0.3.3", 4583 + "gopd": "^1.0.1", 4584 + "is-typed-array": "^1.1.13", 4585 + "possible-typed-array-names": "^1.0.0", 4586 + "reflect.getprototypeof": "^1.0.6" 4587 + }, 4588 + "engines": { 4589 + "node": ">= 0.4" 4590 + }, 4591 + "funding": { 4592 + "url": "https://github.com/sponsors/ljharb" 4593 + } 4594 + }, 4595 + "node_modules/unbox-primitive": { 4596 + "version": "1.1.0", 4597 + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", 4598 + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", 4599 + "dev": true, 4600 + "license": "MIT", 4601 + "dependencies": { 4602 + "call-bound": "^1.0.3", 4603 + "has-bigints": "^1.0.2", 4604 + "has-symbols": "^1.1.0", 4605 + "which-boxed-primitive": "^1.1.1" 4606 + }, 4607 + "engines": { 4608 + "node": ">= 0.4" 4609 + }, 4610 + "funding": { 4611 + "url": "https://github.com/sponsors/ljharb" 4612 + } 4613 + }, 1737 4614 "node_modules/update-browserslist-db": { 1738 4615 "version": "1.2.3", 1739 4616 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", ··· 1763 4640 }, 1764 4641 "peerDependencies": { 1765 4642 "browserslist": ">= 4.21.0" 4643 + } 4644 + }, 4645 + "node_modules/uri-js": { 4646 + "version": "4.4.1", 4647 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 4648 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 4649 + "dev": true, 4650 + "license": "BSD-2-Clause", 4651 + "dependencies": { 4652 + "punycode": "^2.1.0" 1766 4653 } 1767 4654 }, 1768 4655 "node_modules/vite": { ··· 1841 4728 } 1842 4729 } 1843 4730 }, 4731 + "node_modules/which": { 4732 + "version": "2.0.2", 4733 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 4734 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 4735 + "dev": true, 4736 + "license": "ISC", 4737 + "dependencies": { 4738 + "isexe": "^2.0.0" 4739 + }, 4740 + "bin": { 4741 + "node-which": "bin/node-which" 4742 + }, 4743 + "engines": { 4744 + "node": ">= 8" 4745 + } 4746 + }, 4747 + "node_modules/which-boxed-primitive": { 4748 + "version": "1.1.1", 4749 + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", 4750 + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", 4751 + "dev": true, 4752 + "license": "MIT", 4753 + "dependencies": { 4754 + "is-bigint": "^1.1.0", 4755 + "is-boolean-object": "^1.2.1", 4756 + "is-number-object": "^1.1.1", 4757 + "is-string": "^1.1.1", 4758 + "is-symbol": "^1.1.1" 4759 + }, 4760 + "engines": { 4761 + "node": ">= 0.4" 4762 + }, 4763 + "funding": { 4764 + "url": "https://github.com/sponsors/ljharb" 4765 + } 4766 + }, 4767 + "node_modules/which-builtin-type": { 4768 + "version": "1.2.1", 4769 + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", 4770 + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", 4771 + "dev": true, 4772 + "license": "MIT", 4773 + "dependencies": { 4774 + "call-bound": "^1.0.2", 4775 + "function.prototype.name": "^1.1.6", 4776 + "has-tostringtag": "^1.0.2", 4777 + "is-async-function": "^2.0.0", 4778 + "is-date-object": "^1.1.0", 4779 + "is-finalizationregistry": "^1.1.0", 4780 + "is-generator-function": "^1.0.10", 4781 + "is-regex": "^1.2.1", 4782 + "is-weakref": "^1.0.2", 4783 + "isarray": "^2.0.5", 4784 + "which-boxed-primitive": "^1.1.0", 4785 + "which-collection": "^1.0.2", 4786 + "which-typed-array": "^1.1.16" 4787 + }, 4788 + "engines": { 4789 + "node": ">= 0.4" 4790 + }, 4791 + "funding": { 4792 + "url": "https://github.com/sponsors/ljharb" 4793 + } 4794 + }, 4795 + "node_modules/which-collection": { 4796 + "version": "1.0.2", 4797 + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", 4798 + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", 4799 + "dev": true, 4800 + "license": "MIT", 4801 + "dependencies": { 4802 + "is-map": "^2.0.3", 4803 + "is-set": "^2.0.3", 4804 + "is-weakmap": "^2.0.2", 4805 + "is-weakset": "^2.0.3" 4806 + }, 4807 + "engines": { 4808 + "node": ">= 0.4" 4809 + }, 4810 + "funding": { 4811 + "url": "https://github.com/sponsors/ljharb" 4812 + } 4813 + }, 4814 + "node_modules/which-typed-array": { 4815 + "version": "1.1.20", 4816 + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", 4817 + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", 4818 + "dev": true, 4819 + "license": "MIT", 4820 + "dependencies": { 4821 + "available-typed-arrays": "^1.0.7", 4822 + "call-bind": "^1.0.8", 4823 + "call-bound": "^1.0.4", 4824 + "for-each": "^0.3.5", 4825 + "get-proto": "^1.0.1", 4826 + "gopd": "^1.2.0", 4827 + "has-tostringtag": "^1.0.2" 4828 + }, 4829 + "engines": { 4830 + "node": ">= 0.4" 4831 + }, 4832 + "funding": { 4833 + "url": "https://github.com/sponsors/ljharb" 4834 + } 4835 + }, 4836 + "node_modules/word-wrap": { 4837 + "version": "1.2.5", 4838 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 4839 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 4840 + "dev": true, 4841 + "license": "MIT", 4842 + "engines": { 4843 + "node": ">=0.10.0" 4844 + } 4845 + }, 1844 4846 "node_modules/yallist": { 1845 4847 "version": "3.1.1", 1846 4848 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1847 4849 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 1848 4850 "dev": true, 1849 4851 "license": "ISC" 4852 + }, 4853 + "node_modules/yocto-queue": { 4854 + "version": "0.1.0", 4855 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 4856 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 4857 + "dev": true, 4858 + "license": "MIT", 4859 + "engines": { 4860 + "node": ">=10" 4861 + }, 4862 + "funding": { 4863 + "url": "https://github.com/sponsors/sindresorhus" 4864 + } 4865 + }, 4866 + "node_modules/zod": { 4867 + "version": "4.3.5", 4868 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", 4869 + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", 4870 + "dev": true, 4871 + "license": "MIT", 4872 + "peer": true, 4873 + "funding": { 4874 + "url": "https://github.com/sponsors/colinhacks" 4875 + } 4876 + }, 4877 + "node_modules/zod-validation-error": { 4878 + "version": "4.0.2", 4879 + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", 4880 + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", 4881 + "dev": true, 4882 + "license": "MIT", 4883 + "engines": { 4884 + "node": ">=18.0.0" 4885 + }, 4886 + "peerDependencies": { 4887 + "zod": "^3.25.0 || ^4.0.0" 4888 + } 1850 4889 } 1851 4890 } 1852 4891 }
+7
web/package.json
··· 6 6 "scripts": { 7 7 "dev": "vite", 8 8 "build": "vite build", 9 + "lint": "eslint .", 9 10 "preview": "vite preview" 10 11 }, 11 12 "dependencies": { ··· 16 17 "react-router-dom": "^6.28.0" 17 18 }, 18 19 "devDependencies": { 20 + "@eslint/js": "^9.39.2", 19 21 "@types/react": "^18.3.12", 20 22 "@types/react-dom": "^18.3.1", 21 23 "@vitejs/plugin-react": "^4.3.3", 24 + "eslint": "^9.39.2", 25 + "eslint-plugin-react": "^7.37.5", 26 + "eslint-plugin-react-hooks": "^7.0.1", 27 + "eslint-plugin-react-refresh": "^0.4.26", 28 + "globals": "^17.0.0", 22 29 "vite": "^6.0.3" 23 30 } 24 31 }
+2 -1
web/src/App.jsx
··· 15 15 import Collections from "./pages/Collections"; 16 16 import CollectionDetail from "./pages/CollectionDetail"; 17 17 import Privacy from "./pages/Privacy"; 18 - 19 18 import Terms from "./pages/Terms"; 19 + import ScrollToTop from "./components/ScrollToTop"; 20 20 21 21 function AppContent() { 22 22 return ( 23 23 <div className="layout"> 24 + <ScrollToTop /> 24 25 <Sidebar /> 25 26 <div className="main-layout"> 26 27 <main className="main-content-wrapper">
+15
web/src/api/client.js
··· 430 430 export async function getTrendingTags(limit = 10) { 431 431 return request(`${API_BASE}/tags/trending?limit=${limit}`); 432 432 } 433 + 434 + export async function getAPIKeys() { 435 + return request(`${API_BASE}/keys`); 436 + } 437 + 438 + export async function createAPIKey(name) { 439 + return request(`${API_BASE}/keys`, { 440 + method: "POST", 441 + body: JSON.stringify({ name }), 442 + }); 443 + } 444 + 445 + export async function deleteAPIKey(id) { 446 + return request(`${API_BASE}/keys/${id}`, { method: "DELETE" }); 447 + }
+4 -4
web/src/components/AddToCollectionModal.jsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 2 import { X, Plus, Check, Folder } from "lucide-react"; 3 3 import { 4 4 getCollections, ··· 30 30 loadCollections(); 31 31 setError(null); 32 32 } 33 - }, [isOpen, user, annotationUri]); 33 + }, [isOpen, user, annotationUri, loadCollections]); 34 34 35 - const loadCollections = async () => { 35 + const loadCollections = useCallback(async () => { 36 36 try { 37 37 setLoading(true); 38 38 const [data, existingURIs] = await Promise.all([ ··· 49 49 } finally { 50 50 setLoading(false); 51 51 } 52 - }; 52 + }, [user?.did, annotationUri]); 53 53 54 54 const handleAdd = async (collectionUri) => { 55 55 if (addedTo.has(collectionUri)) return;
+37 -49
web/src/components/AnnotationCard.jsx
··· 5 5 import { 6 6 normalizeAnnotation, 7 7 normalizeHighlight, 8 - normalizeBookmark, 9 - deleteAnnotation, 10 8 likeAnnotation, 11 9 unlikeAnnotation, 12 10 getReplies, 13 11 createReply, 14 12 deleteReply, 15 - getLikeCount, 16 13 updateAnnotation, 17 14 updateHighlight, 18 - updateBookmark, 19 15 getEditHistory, 16 + deleteAnnotation, 20 17 } from "../api/client"; 21 18 import { 22 - HeartIcon, 23 - MessageIcon, 24 - TrashIcon, 25 - ExternalLinkIcon, 26 - HighlightIcon, 27 - BookmarkIcon, 28 - } from "./Icons"; 29 - import { Folder, Edit2, Save, X, Clock } from "lucide-react"; 19 + MessageSquare, 20 + Heart, 21 + Trash2, 22 + Folder, 23 + Edit2, 24 + Save, 25 + X, 26 + Clock, 27 + } from "lucide-react"; 28 + import { HighlightIcon, TrashIcon } from "./Icons"; 30 29 import ShareMenu from "./ShareMenu"; 31 30 32 31 function buildTextFragmentUrl(baseUrl, selector) { ··· 90 89 91 90 const [hasEditHistory, setHasEditHistory] = useState(false); 92 91 93 - useEffect(() => {}, []); 92 + useEffect(() => { 93 + if (data.uri && !data.color && !data.description) { 94 + getEditHistory(data.uri) 95 + .then((history) => { 96 + if (history && history.length > 0) { 97 + setHasEditHistory(true); 98 + } 99 + }) 100 + .catch(() => {}); 101 + } 102 + }, [data.uri, data.color, data.description]); 94 103 95 104 const fetchHistory = async () => { 96 105 if (showHistory) { ··· 216 225 } 217 226 }; 218 227 219 - const handleShare = async () => { 220 - const uriParts = data.uri.split("/"); 221 - const did = uriParts[2]; 222 - const rkey = uriParts[uriParts.length - 1]; 223 - const shareUrl = `${window.location.origin}/at/${did}/${rkey}`; 224 - 225 - if (navigator.share) { 226 - try { 227 - await navigator.share({ 228 - title: "Margin Annotation", 229 - text: data.text?.substring(0, 100), 230 - url: shareUrl, 231 - }); 232 - } catch (err) {} 233 - } else { 234 - try { 235 - await navigator.clipboard.writeText(shareUrl); 236 - alert("Link copied!"); 237 - } catch { 238 - prompt("Copy this link:", shareUrl); 239 - } 240 - } 241 - }; 242 - 243 228 const handleDelete = async () => { 244 229 if (!confirm("Delete this annotation? This cannot be undone.")) return; 245 230 try { ··· 324 309 disabled={deleting} 325 310 title="Delete" 326 311 > 327 - <TrashIcon size={16} /> 312 + <Trash2 size={16} /> 328 313 </button> 329 314 </> 330 315 )} ··· 386 371 borderLeftColor: data.color || "var(--accent)", 387 372 }} 388 373 > 389 - <mark>"{highlightedText}"</mark> 374 + <mark>&quot;{highlightedText}&quot;</mark> 390 375 </a> 391 376 )} 392 377 ··· 454 439 className={`annotation-action ${isLiked ? "liked" : ""}`} 455 440 onClick={handleLike} 456 441 > 457 - <HeartIcon filled={isLiked} size={16} /> 442 + <Heart filled={isLiked} size={16} /> 458 443 {likeCount > 0 && <span>{likeCount}</span>} 459 444 </button> 460 445 <button ··· 471 456 setShowReplies(!showReplies); 472 457 }} 473 458 > 474 - <MessageIcon size={16} /> 459 + <MessageSquare size={16} /> 475 460 <span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span> 476 461 </button> 477 462 <ShareMenu ··· 561 546 onChange={(e) => setReplyText(e.target.value)} 562 547 onFocus={(e) => { 563 548 if (!user) { 564 - e.target.blur(); 565 - login(); 549 + e.preventDefault(); 550 + alert("Please sign in to like annotations"); 566 551 } 567 552 }} 568 553 rows={2} ··· 589 574 ); 590 575 } 591 576 592 - export function HighlightCard({ highlight, onDelete, onAddToCollection }) { 577 + export function HighlightCard({ 578 + highlight, 579 + onDelete, 580 + onAddToCollection, 581 + onUpdate, 582 + }) { 593 583 const { user, login } = useAuth(); 594 584 const data = normalizeHighlight(highlight); 595 585 const highlightedText = ··· 609 599 610 600 await updateHighlight(data.uri, editColor, tagList); 611 601 setIsEditing(false); 612 - 613 - if (highlight.color) highlight.color = editColor; 614 - if (highlight.tags) highlight.tags = tagList; 615 - else highlight.value = { ...highlight.value, tags: tagList }; 602 + if (typeof onUpdate === "function") 603 + onUpdate({ ...highlight, color: editColor, tags: tagList }); 616 604 } catch (err) { 617 605 alert("Failed to update: " + err.message); 618 606 } ··· 720 708 borderLeftColor: isEditing ? editColor : data.color || "#f59e0b", 721 709 }} 722 710 > 723 - <mark>"{highlightedText}"</mark> 711 + <mark>&quot;{highlightedText}&quot;</mark> 724 712 </a> 725 713 )} 726 714
-2
web/src/components/AnnotationSkeleton.jsx
··· 1 - import React from "react"; 2 - 3 1 export default function AnnotationSkeleton() { 4 2 return ( 5 3 <div className="skeleton-card">
+20 -10
web/src/components/BookmarkCard.jsx
··· 9 9 getLikeCount, 10 10 deleteBookmark, 11 11 } from "../api/client"; 12 - import { HeartIcon, TrashIcon, ExternalLinkIcon, BookmarkIcon } from "./Icons"; 12 + import { HeartIcon, TrashIcon, BookmarkIcon } from "./Icons"; 13 13 import { Folder } from "lucide-react"; 14 14 import ShareMenu from "./ShareMenu"; 15 15 16 - export default function BookmarkCard({ bookmark, onAddToCollection }) { 16 + export default function BookmarkCard({ 17 + bookmark, 18 + onAddToCollection, 19 + onDelete, 20 + }) { 17 21 const { user, login } = useAuth(); 18 22 const raw = bookmark; 19 23 const data = ··· 34 38 if (likeRes.count !== undefined) setLikeCount(likeRes.count); 35 39 if (likeRes.liked !== undefined) setIsLiked(likeRes.liked); 36 40 } 37 - } catch (err) { 38 - console.error("Failed to fetch data:", err); 41 + } catch { 42 + /* ignore */ 39 43 } 40 44 } 41 45 if (data.uri) fetchData(); ··· 60 64 const cid = data.cid || ""; 61 65 if (data.uri && cid) await likeAnnotation(data.uri, cid); 62 66 } 63 - } catch (err) { 67 + } catch { 64 68 setIsLiked(!isLiked); 65 69 setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1)); 66 70 } 67 71 }; 68 72 69 73 const handleDelete = async () => { 74 + if (onDelete) { 75 + onDelete(data.uri); 76 + return; 77 + } 78 + 70 79 if (!confirm("Delete this bookmark?")) return; 71 80 try { 72 81 setDeleting(true); 73 82 const parts = data.uri.split("/"); 74 83 const rkey = parts[parts.length - 1]; 75 84 await deleteBookmark(rkey); 76 - if (onDelete) onDelete(data.uri); 77 - else window.location.reload(); 85 + window.location.reload(); 78 86 } catch (err) { 79 87 alert("Failed to delete: " + err.message); 80 88 } finally { ··· 100 108 let domain = ""; 101 109 try { 102 110 if (data.url) domain = new URL(data.url).hostname.replace("www.", ""); 103 - } catch {} 111 + } catch { 112 + /* ignore */ 113 + } 104 114 105 115 const authorDisplayName = data.author?.displayName || data.author?.handle; 106 116 const authorHandle = data.author?.handle; ··· 109 119 const marginProfileUrl = authorDid ? `/profile/${authorDid}` : null; 110 120 111 121 return ( 112 - <article className="card bookmark-card"> 122 + <article className="card annotation-card bookmark-card"> 113 123 <header className="annotation-header"> 114 124 <div className="annotation-header-left"> 115 125 <Link to={marginProfileUrl || "#"} className="annotation-avatar-link"> ··· 150 160 151 161 <div className="annotation-header-right"> 152 162 <div style={{ display: "flex", gap: "4px" }}> 153 - {isOwner && ( 163 + {(isOwner || onDelete) && ( 154 164 <button 155 165 className="annotation-action action-icon-only" 156 166 onClick={handleDelete}
-1
web/src/components/CollectionItemCard.jsx
··· 1 - import React from "react"; 2 1 import { Link } from "react-router-dom"; 3 2 import AnnotationCard, { HighlightCard } from "./AnnotationCard"; 4 3 import BookmarkCard from "./BookmarkCard";
-7
web/src/components/CollectionModal.jsx
··· 12 12 Camera, 13 13 Code, 14 14 Globe, 15 - Lock, 16 15 Flag, 17 16 Tag, 18 17 Box, ··· 21 20 Image, 22 21 Video, 23 22 Mail, 24 - Phone, 25 23 MapPin, 26 24 Calendar, 27 25 Clock, ··· 31 29 Users, 32 30 Home, 33 31 Briefcase, 34 - ShoppingBag, 35 32 Gift, 36 33 Award, 37 34 Target, 38 35 TrendingUp, 39 - BarChart, 40 - PieChart, 41 36 Activity, 42 37 Cpu, 43 38 Database, ··· 46 41 Moon, 47 42 Flame, 48 43 Leaf, 49 - Droplet, 50 - Snowflake, 51 44 } from "lucide-react"; 52 45 import { createCollection, updateCollection } from "../api/client"; 53 46
+1 -1
web/src/components/Composer.jsx
··· 91 91 ร— 92 92 </button> 93 93 <blockquote> 94 - <mark className="quote-exact">"{highlightedText}"</mark> 94 + <mark className="quote-exact">&quot;{highlightedText}&quot;</mark> 95 95 </blockquote> 96 96 </div> 97 97 )}
-1
web/src/components/ReplyList.jsx
··· 1 - import React from "react"; 2 1 import { Link } from "react-router-dom"; 3 2 import { MessageSquare, Trash2, Reply } from "lucide-react"; 4 3
+44 -13
web/src/components/RightSidebar.jsx
··· 1 1 import { useState, useEffect } from "react"; 2 2 import { Link } from "react-router-dom"; 3 - import { Download, ExternalLink } from "lucide-react"; 4 - import { SiFirefox, SiGooglechrome, SiGithub, SiBluesky } from "react-icons/si"; 3 + import { ExternalLink } from "lucide-react"; 4 + import { 5 + SiFirefox, 6 + SiGooglechrome, 7 + SiGithub, 8 + SiBluesky, 9 + SiApple, 10 + SiKofi, 11 + } from "react-icons/si"; 5 12 import { FaEdge } from "react-icons/fa"; 6 13 import { useAuth } from "../context/AuthContext"; 7 14 import { getTrendingTags } from "../api/client"; 8 - import tangledIcon from "../assets/tangled.svg"; 9 15 10 16 const isFirefox = 11 17 typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 12 18 const isEdge = 13 19 typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 14 - const isChrome = 20 + const isMobileSafari = 15 21 typeof navigator !== "undefined" && 16 - /Chrome/i.test(navigator.userAgent) && 17 - !isEdge; 22 + /iPhone|iPad|iPod/.test(navigator.userAgent) && 23 + /Safari/.test(navigator.userAgent) && 24 + !/CriOS|FxiOS|OPiOS|EdgiOS/.test(navigator.userAgent); 18 25 19 26 function getExtensionInfo() { 27 + if (isMobileSafari) { 28 + return { 29 + url: "https://margin.at/soon", 30 + icon: SiApple, 31 + name: "iOS", 32 + label: "Coming Soon", 33 + }; 34 + } 20 35 if (isFirefox) { 21 36 return { 22 37 url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", 23 38 icon: SiFirefox, 24 39 name: "Firefox", 40 + label: "Install for Firefox", 25 41 }; 26 42 } 27 43 if (isEdge) { ··· 29 45 url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", 30 46 icon: FaEdge, 31 47 name: "Edge", 48 + label: "Install for Edge", 32 49 }; 33 50 } 34 51 return { 35 52 url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", 36 53 icon: SiGooglechrome, 37 54 name: "Chrome", 55 + label: "Install for Chrome", 38 56 }; 39 57 } 40 58 ··· 55 73 return ( 56 74 <aside className="right-sidebar"> 57 75 <div className="right-section"> 58 - <h3 className="right-section-title">Get the Extension</h3> 76 + <h3 className="right-section-title"> 77 + {isMobileSafari ? "Save from Safari" : "Get the Extension"} 78 + </h3> 59 79 <p className="right-section-desc"> 60 - Annotate, highlight, and bookmark any webpage 80 + {isMobileSafari 81 + ? "Bookmark pages using Safari's share sheet" 82 + : "Annotate, highlight, and bookmark any webpage"} 61 83 </p> 62 84 <a 63 85 href={ext.url} ··· 66 88 className="right-extension-btn" 67 89 > 68 90 <ExtIcon size={18} /> 69 - Install for {ext.name} 91 + {ext.label} 70 92 <ExternalLink size={14} /> 71 93 </a> 72 94 </div> ··· 101 123 <nav className="right-links"> 102 124 <Link to="/url" className="right-link"> 103 125 Browse by URL 104 - </Link> 105 - <Link to="/highlights" className="right-link"> 106 - Public Highlights 107 126 </Link> 108 127 </nav> 109 128 </div> ··· 125 144 <ExternalLink size={12} /> 126 145 </a> 127 146 <a 128 - href="https://tangled.net" 147 + href="https://tangled.org/margin.at/margin" 129 148 target="_blank" 130 149 rel="noopener noreferrer" 131 150 className="right-link" ··· 145 164 <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 146 165 <SiBluesky size={16} /> 147 166 Bluesky 167 + </div> 168 + <ExternalLink size={12} /> 169 + </a> 170 + <a 171 + href="https://ko-fi.com/scan" 172 + target="_blank" 173 + rel="noopener noreferrer" 174 + className="right-link" 175 + > 176 + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> 177 + <SiKofi size={16} /> 178 + Donate 148 179 </div> 149 180 <ExternalLink size={12} /> 150 181 </a>
+12
web/src/components/ScrollToTop.jsx
··· 1 + import { useEffect } from "react"; 2 + import { useLocation } from "react-router-dom"; 3 + 4 + export default function ScrollToTop() { 5 + const { pathname } = useLocation(); 6 + 7 + useEffect(() => { 8 + window.scrollTo(0, 0); 9 + }, [pathname]); 10 + 11 + return null; 12 + }
+3 -1
web/src/components/ShareMenu.jsx
··· 171 171 text: text?.substring(0, 100), 172 172 url: shareUrl, 173 173 }); 174 - } catch {} 174 + } catch { 175 + /* ignore */ 176 + } 175 177 } 176 178 setIsOpen(false); 177 179 };
+4 -1
web/src/context/AuthContext.jsx
··· 48 48 const handleLogout = async () => { 49 49 try { 50 50 await logout(); 51 - } catch {} 51 + } catch (e) { 52 + console.warn("Logout failed", e); 53 + } 52 54 setUser(null); 53 55 }; 54 56 ··· 64 66 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; 65 67 } 66 68 69 + // eslint-disable-next-line react-refresh/only-export-components 67 70 export function useAuth() { 68 71 const context = useContext(AuthContext); 69 72 if (!context) {
+34 -1
web/src/css/annotations.css
··· 182 182 font-weight: 400; 183 183 font-family: var(--font-serif, var(--font-sans)); 184 184 display: inline; 185 + overflow-wrap: anywhere; 186 + word-break: break-all; 187 + padding-right: 4px; 185 188 } 186 189 187 190 .annotation-text { ··· 277 280 padding-left: 46px; 278 281 } 279 282 280 - @media (max-width: 600px) { 283 + .annotation-text, 284 + .reply-text, 285 + .history-content { 286 + overflow-wrap: break-word; 287 + word-break: break-word; 288 + max-width: 100%; 289 + } 290 + 291 + .annotation-highlight mark { 292 + overflow-wrap: break-word; 293 + word-break: break-word; 294 + display: inline; 295 + } 296 + 297 + .annotation-header-left, 298 + .annotation-meta, 299 + .reply-meta { 300 + min-width: 0; 301 + max-width: 100%; 302 + } 303 + 304 + .annotation-author-row, 305 + .reply-author { 306 + max-width: 100%; 307 + } 308 + 309 + .annotation-source { 310 + max-width: 100%; 311 + } 312 + 313 + @media (max-width: 768px) { 281 314 .annotation-content, 282 315 .annotation-actions, 283 316 .inline-replies {
+3
web/src/css/base.css
··· 41 41 html { 42 42 font-size: 16px; 43 43 -webkit-text-size-adjust: 100%; 44 + overflow-x: hidden; 44 45 } 45 46 46 47 body { ··· 51 52 min-height: 100vh; 52 53 -webkit-font-smoothing: antialiased; 53 54 -moz-osx-font-smoothing: grayscale; 55 + overflow-x: hidden; 56 + max-width: 100vw; 54 57 } 55 58 56 59 a {
+2
web/src/css/buttons.css
··· 8 8 font-weight: 500; 9 9 border-radius: var(--radius-md); 10 10 transition: all 0.15s ease; 11 + white-space: pre; 11 12 } 12 13 13 14 .btn-primary { ··· 118 119 .action-buttons { 119 120 display: flex; 120 121 gap: 8px; 122 + flex-wrap: wrap; 121 123 } 122 124 123 125 .action-buttons-end {
+16
web/src/css/collections.css
··· 171 171 line-height: 1.3; 172 172 } 173 173 174 + @media (max-width: 600px) { 175 + .collection-detail-header { 176 + flex-direction: column; 177 + padding: 16px; 178 + gap: 16px; 179 + } 180 + 181 + .collection-detail-actions { 182 + position: static; 183 + margin-top: -8px; 184 + justify-content: flex-end; 185 + } 186 + } 187 + 174 188 .collection-detail-desc { 175 189 color: var(--text-secondary); 176 190 font-size: 1rem; 177 191 line-height: 1.5; 178 192 margin-bottom: 12px; 179 193 max-width: 600px; 194 + overflow-wrap: break-word; 195 + word-break: break-word; 180 196 } 181 197 182 198 .collection-detail-stats {
+2
web/src/css/feed.css
··· 24 24 background: var(--bg-tertiary); 25 25 border-radius: var(--radius-lg); 26 26 width: fit-content; 27 + max-width: 100%; 28 + flex-wrap: wrap; 27 29 } 28 30 29 31 .filter-tab {
+50
web/src/css/layout.css
··· 451 451 .main-layout { 452 452 margin-left: 0; 453 453 padding-bottom: 80px; 454 + width: 100%; 455 + min-width: 0; 454 456 } 455 457 456 458 .main-content-wrapper { 457 459 padding: 20px 16px; 460 + max-width: 100%; 461 + width: 100%; 462 + overflow-x: hidden; 463 + min-width: 0; 458 464 } 459 465 460 466 .mobile-nav { 461 467 display: block; 468 + max-width: 100vw; 469 + } 470 + 471 + .card, 472 + .annotation-card, 473 + .collection-card, 474 + .profile-header, 475 + .api-keys-section { 476 + overflow-x: hidden; 477 + max-width: 100%; 478 + } 479 + 480 + code { 481 + word-break: break-all; 482 + overflow-wrap: break-word; 483 + } 484 + 485 + pre { 486 + overflow-x: auto; 487 + max-width: 100%; 488 + } 489 + 490 + input, 491 + textarea { 492 + max-width: 100%; 493 + } 494 + 495 + .flex-row, 496 + [style*="display: flex"][style*="gap"] { 497 + flex-wrap: wrap; 498 + } 499 + 500 + .static-page { 501 + overflow-x: hidden; 502 + } 503 + 504 + .static-page ol, 505 + .static-page ul { 506 + padding-left: 1.25rem; 507 + } 508 + 509 + .static-page code { 510 + font-size: 0.75rem; 511 + word-break: break-all; 462 512 } 463 513 }
+118 -107
web/src/css/login.css
··· 3 3 flex-direction: column; 4 4 align-items: center; 5 5 justify-content: center; 6 - min-height: 70vh; 7 - padding: 60px 20px; 6 + min-height: 80vh; 7 + padding: 40px 20px; 8 8 width: 100%; 9 - max-width: 500px; 10 - margin: 0 auto; 11 9 } 12 10 13 - .login-at-logo { 14 - font-size: 5rem; 15 - font-weight: 800; 16 - color: var(--accent); 17 - margin-bottom: 24px; 18 - line-height: 1; 11 + .login-header-group { 12 + display: flex; 13 + flex-direction: row; 14 + align-items: center; 15 + justify-content: center; 16 + gap: 24px; 17 + margin-bottom: 48px; 18 + width: auto; 19 19 } 20 20 21 21 .login-logo-img { 22 - width: 80px; 23 - height: 80px; 24 - margin-bottom: 24px; 22 + width: 60px; 23 + height: 60px; 25 24 object-fit: contain; 25 + display: block; 26 + } 27 + 28 + .login-x { 29 + font-size: 2rem; 30 + color: var(--text-tertiary); 31 + font-weight: 300; 32 + line-height: 1; 33 + padding-bottom: 4px; 34 + } 35 + 36 + .login-atproto-icon { 37 + color: #3b83f6 !important; 38 + display: flex; 39 + align-items: center; 40 + justify-content: center; 26 41 } 27 42 28 43 .login-heading { 29 44 font-size: 1.5rem; 30 - font-weight: 600; 45 + font-weight: 700; 31 46 margin-bottom: 32px; 32 47 display: flex; 33 48 align-items: center; 34 - gap: 10px; 49 + justify-content: center; 50 + gap: 8px; 35 51 text-align: center; 36 - line-height: 1.4; 52 + line-height: 1.3; 53 + color: var(--text-primary); 37 54 } 38 55 39 56 .login-help-btn { ··· 53 70 } 54 71 55 72 .login-help-text { 56 - background: var(--bg-elevated); 73 + background: var(--bg-tertiary); 57 74 border: 1px solid var(--border); 58 75 border-radius: var(--radius-md); 59 - padding: 16px 20px; 76 + padding: 16px; 60 77 margin-bottom: 24px; 61 - font-size: 0.95rem; 78 + font-size: 0.9rem; 62 79 color: var(--text-secondary); 63 - line-height: 1.6; 80 + line-height: 1.5; 64 81 text-align: center; 82 + width: 100%; 65 83 } 66 84 67 85 .login-help-text code { 68 - background: var(--bg-tertiary); 69 - padding: 2px 8px; 86 + background: rgba(255, 255, 255, 0.05); 87 + padding: 2px 6px; 70 88 border-radius: var(--radius-sm); 71 - font-size: 0.9rem; 89 + font-size: 0.85rem; 90 + font-family: var(--font-mono); 72 91 } 73 92 74 93 .login-form { 75 94 display: flex; 76 95 flex-direction: column; 77 - gap: 16px; 96 + gap: 20px; 78 97 width: 100%; 79 98 } 80 99 ··· 85 104 .login-input { 86 105 width: 100%; 87 106 padding: 14px 16px; 88 - background: var(--bg-elevated); 107 + background: var(--bg-secondary); 89 108 border: 1px solid var(--border); 90 109 border-radius: var(--radius-md); 91 110 color: var(--text-primary); 92 111 font-size: 1rem; 93 - transition: 94 - border-color 0.15s, 95 - box-shadow 0.15s; 112 + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 113 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 96 114 } 97 115 98 116 .login-input:focus { 99 117 outline: none; 100 118 border-color: var(--accent); 101 - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); 119 + box-shadow: 0 0 0 4px var(--accent-subtle); 120 + background: var(--bg-primary); 102 121 } 103 122 104 123 .login-input::placeholder { ··· 107 126 108 127 .login-suggestions { 109 128 position: absolute; 110 - top: calc(100% + 4px); 129 + top: calc(100% + 8px); 111 130 left: 0; 112 131 right: 0; 113 - background: var(--bg-card); 132 + background: var(--bg-elevated); 114 133 border: 1px solid var(--border); 115 134 border-radius: var(--radius-md); 116 135 box-shadow: var(--shadow-lg); 117 136 overflow: hidden; 118 137 z-index: 100; 138 + max-height: 300px; 139 + overflow-y: auto; 119 140 } 120 141 121 142 .login-suggestion { ··· 129 150 cursor: pointer; 130 151 text-align: left; 131 152 transition: background 0.1s; 153 + border-bottom: 1px solid var(--border); 154 + } 155 + 156 + .login-suggestion:last-child { 157 + border-bottom: none; 132 158 } 133 159 134 160 .login-suggestion:hover, 135 161 .login-suggestion.selected { 136 - background: var(--bg-elevated); 162 + background: var(--bg-tertiary); 137 163 } 138 164 139 165 .login-suggestion-avatar { 140 - width: 40px; 141 - height: 40px; 166 + width: 36px; 167 + height: 36px; 142 168 border-radius: var(--radius-full); 143 169 background: linear-gradient(135deg, var(--accent), #a855f7); 144 170 display: flex; ··· 146 172 justify-content: center; 147 173 flex-shrink: 0; 148 174 overflow: hidden; 149 - font-size: 0.875rem; 175 + font-size: 0.8rem; 150 176 font-weight: 600; 151 177 color: white; 152 178 } ··· 161 187 display: flex; 162 188 flex-direction: column; 163 189 min-width: 0; 190 + gap: 2px; 164 191 } 165 192 166 193 .login-suggestion-name { 167 194 font-weight: 600; 195 + font-size: 0.95rem; 168 196 color: var(--text-primary); 169 197 white-space: nowrap; 170 198 overflow: hidden; ··· 172 200 } 173 201 174 202 .login-suggestion-handle { 175 - font-size: 0.875rem; 203 + font-size: 0.85rem; 176 204 color: var(--text-secondary); 177 205 white-space: nowrap; 178 206 overflow: hidden; ··· 182 210 .login-error { 183 211 padding: 12px 16px; 184 212 background: rgba(239, 68, 68, 0.1); 185 - border: 1px solid rgba(239, 68, 68, 0.3); 213 + border: 1px solid rgba(239, 68, 68, 0.2); 186 214 border-radius: var(--radius-md); 187 - color: #ef4444; 215 + color: var(--error); 188 216 font-size: 0.875rem; 217 + text-align: center; 189 218 } 190 219 191 - .login-legal { 192 - font-size: 0.75rem; 193 - color: var(--text-tertiary); 194 - line-height: 1.5; 195 - margin-top: 16px; 196 - } 197 - 198 - .login-brand { 199 - display: flex; 200 - align-items: center; 220 + .login-submit { 221 + padding: 14px 24px; 222 + font-size: 1rem; 223 + font-weight: 600; 224 + width: 100%; 201 225 justify-content: center; 202 - gap: 12px; 203 - margin-bottom: 24px; 204 - } 205 - 206 - .login-brand-icon { 207 - width: 48px; 208 - height: 48px; 209 - background: linear-gradient(135deg, var(--accent), #a855f7); 210 - border-radius: var(--radius-lg); 211 - display: flex; 212 - align-items: center; 213 - justify-content: center; 214 - font-size: 1.75rem; 215 - font-weight: 800; 216 - color: white; 217 - } 218 - 219 - .login-brand-name { 220 - font-size: 1.75rem; 221 - font-weight: 700; 222 - } 223 - 224 - .login-avatar { 225 - width: 72px; 226 - height: 72px; 227 - border-radius: var(--radius-full); 228 - background: linear-gradient(135deg, var(--accent), #a855f7); 229 - display: flex; 230 - align-items: center; 231 - justify-content: center; 232 - margin: 0 auto 16px; 233 - font-weight: 700; 234 - font-size: 1.5rem; 235 - color: white; 236 - overflow: hidden; 237 - } 238 - 239 - .login-avatar img { 240 - width: 100%; 241 - height: 100%; 242 - object-fit: cover; 243 226 } 244 227 245 228 .login-avatar-large { 246 - width: 100px; 247 - height: 100px; 229 + width: 80px; 230 + height: 80px; 248 231 border-radius: var(--radius-full); 249 232 background: linear-gradient(135deg, var(--accent), #a855f7); 250 233 display: flex; ··· 255 238 font-size: 2rem; 256 239 color: white; 257 240 overflow: hidden; 241 + box-shadow: var(--shadow-md); 258 242 } 259 243 260 244 .login-avatar-large img { ··· 264 248 } 265 249 266 250 .login-welcome { 267 - font-size: 1.5rem; 251 + font-size: 1.25rem; 268 252 font-weight: 600; 269 253 margin-bottom: 32px; 270 254 text-align: center; 271 - } 272 - 273 - .login-welcome-name { 274 - font-size: 1.25rem; 275 - font-weight: 600; 276 - margin-bottom: 24px; 255 + color: var(--text-primary); 277 256 } 278 257 279 258 .login-actions { ··· 283 262 width: 100%; 284 263 } 285 264 286 - .login-btn { 287 - width: 100%; 288 - padding: 14px 24px; 289 - font-size: 1rem; 290 - font-weight: 600; 265 + .morph-container { 266 + display: inline-block; 267 + color: var(--text-primary); 268 + font-weight: 700; 269 + transition: 270 + opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), 271 + transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), 272 + filter 0.4s cubic-bezier(0.4, 0, 0.2, 1); 273 + white-space: nowrap; 274 + vertical-align: bottom; 291 275 } 292 276 293 - .login-submit { 294 - padding: 18px 32px; 295 - font-size: 1.1rem; 296 - font-weight: 600; 277 + .morph-out { 278 + opacity: 0; 279 + transform: translateY(8px) scale(0.95); 280 + filter: blur(4px); 281 + } 282 + 283 + .morph-in { 284 + opacity: 1; 285 + transform: translateY(0) scale(1); 286 + filter: blur(0); 287 + } 288 + 289 + .login-legal { 290 + margin-top: 24px; 291 + font-size: 0.85rem; 292 + color: var(--text-tertiary); 293 + text-align: center; 294 + line-height: 1.5; 295 + } 296 + 297 + .login-legal a { 298 + color: var(--accent); 299 + text-decoration: underline; 300 + text-decoration-color: var(--accent); 301 + text-underline-offset: 4px; 302 + font-weight: 500; 303 + } 304 + 305 + .login-legal a:hover { 306 + text-decoration-thickness: 2px; 307 + opacity: 0.8; 297 308 }
+2
web/src/css/notifications.css
··· 53 53 margin-bottom: 4px; 54 54 line-height: 1.4; 55 55 color: var(--text-primary); 56 + overflow-wrap: break-word; 57 + word-break: break-word; 56 58 } 57 59 58 60 .notification-text strong {
+162 -155
web/src/css/profile.css
··· 1 1 .profile-header { 2 - display: flex; 3 - align-items: center; 4 - gap: 24px; 5 - margin-bottom: 32px; 6 - padding-bottom: 24px; 7 - border-bottom: 1px solid var(--border); 2 + display: flex; 3 + align-items: center; 4 + gap: 24px; 5 + margin-bottom: 32px; 6 + padding-bottom: 24px; 7 + border-bottom: 1px solid var(--border); 8 8 } 9 9 10 10 .profile-avatar { 11 - width: 80px; 12 - height: 80px; 13 - min-width: 80px; 14 - border-radius: 50%; 15 - background: var(--bg-tertiary); 16 - display: flex; 17 - align-items: center; 18 - justify-content: center; 19 - font-weight: 600; 20 - font-size: 2rem; 21 - color: var(--text-secondary); 22 - overflow: hidden; 23 - border: 1px solid var(--border); 11 + width: 80px; 12 + height: 80px; 13 + min-width: 80px; 14 + border-radius: 50%; 15 + background: var(--bg-tertiary); 16 + display: flex; 17 + align-items: center; 18 + justify-content: center; 19 + font-weight: 600; 20 + font-size: 2rem; 21 + color: var(--text-secondary); 22 + overflow: hidden; 23 + border: 1px solid var(--border); 24 24 } 25 25 26 26 .profile-avatar img { 27 - width: 100%; 28 - height: 100%; 29 - object-fit: cover; 27 + width: 100%; 28 + height: 100%; 29 + object-fit: cover; 30 30 } 31 31 32 32 .profile-avatar-link { 33 - text-decoration: none; 33 + text-decoration: none; 34 34 } 35 35 36 36 .profile-info { 37 - flex: 1; 38 - display: flex; 39 - flex-direction: column; 40 - gap: 4px; 37 + flex: 1; 38 + display: flex; 39 + flex-direction: column; 40 + gap: 4px; 41 41 } 42 42 43 43 .profile-name { 44 - font-size: 1.5rem; 45 - font-weight: 700; 46 - color: var(--text-primary); 47 - line-height: 1.2; 44 + font-size: 1.5rem; 45 + font-weight: 700; 46 + color: var(--text-primary); 47 + line-height: 1.2; 48 + overflow-wrap: break-word; 49 + word-break: break-word; 48 50 } 49 51 50 52 .profile-handle-row { 51 - display: flex; 52 - align-items: center; 53 - gap: 12px; 54 - margin-top: 4px; 55 - flex-wrap: wrap; 53 + display: flex; 54 + align-items: center; 55 + gap: 12px; 56 + margin-top: 4px; 57 + flex-wrap: wrap; 56 58 } 57 59 58 60 .profile-handle-link { 59 - color: var(--text-tertiary); 60 - text-decoration: none; 61 - font-size: 1rem; 62 - transition: color 0.15s; 61 + color: var(--text-tertiary); 62 + text-decoration: none; 63 + font-size: 1rem; 64 + transition: color 0.15s; 65 + overflow-wrap: break-word; 66 + word-break: break-all; 63 67 } 64 68 65 69 .profile-handle-link:hover { 66 - color: var(--text-secondary); 70 + color: var(--text-secondary); 67 71 } 68 72 69 73 .profile-bluesky-link { 70 - display: inline-flex; 71 - align-items: center; 72 - gap: 6px; 73 - color: #3b82f6; 74 - text-decoration: none; 75 - font-size: 0.85rem; 76 - font-weight: 500; 77 - padding: 2px 8px; 78 - border-radius: var(--radius-sm); 79 - background: rgba(59, 130, 246, 0.1); 80 - transition: all 0.15s ease; 74 + display: inline-flex; 75 + align-items: center; 76 + gap: 6px; 77 + color: #3b82f6; 78 + text-decoration: none; 79 + font-size: 0.85rem; 80 + font-weight: 500; 81 + padding: 2px 8px; 82 + border-radius: var(--radius-sm); 83 + background: rgba(59, 130, 246, 0.1); 84 + transition: all 0.15s ease; 85 + width: fit-content; 81 86 } 82 87 83 88 .profile-bluesky-link:hover { 84 - background: rgba(59, 130, 246, 0.15); 89 + background: rgba(59, 130, 246, 0.15); 85 90 } 86 91 87 92 .profile-stats { 88 - display: flex; 89 - gap: 24px; 90 - margin-top: 12px; 93 + display: flex; 94 + gap: 24px; 95 + margin-top: 12px; 91 96 } 92 97 93 98 .profile-stat { 94 - color: var(--text-tertiary); 95 - font-size: 0.9rem; 99 + color: var(--text-tertiary); 100 + font-size: 0.9rem; 96 101 } 97 102 98 103 .profile-stat strong { 99 - color: var(--text-primary); 100 - font-weight: 600; 104 + color: var(--text-primary); 105 + font-weight: 600; 101 106 } 102 107 103 108 .profile-tabs { 104 - display: flex; 105 - gap: 24px; 106 - margin-bottom: 24px; 107 - border-bottom: 1px solid var(--border); 109 + display: flex; 110 + gap: 24px; 111 + margin-bottom: 24px; 112 + border-bottom: 1px solid var(--border); 113 + flex-wrap: wrap; 114 + row-gap: 8px; 108 115 } 109 116 110 117 .profile-tab { 111 - padding: 12px 0; 112 - font-size: 0.95rem; 113 - font-weight: 500; 114 - color: var(--text-tertiary); 115 - background: transparent; 116 - border: none; 117 - cursor: pointer; 118 - transition: all 0.15s ease; 119 - position: relative; 118 + padding: 12px 0; 119 + font-size: 0.95rem; 120 + font-weight: 500; 121 + color: var(--text-tertiary); 122 + background: transparent; 123 + border: none; 124 + cursor: pointer; 125 + transition: all 0.15s ease; 126 + position: relative; 120 127 } 121 128 122 129 .profile-tab:hover { 123 - color: var(--text-primary); 130 + color: var(--text-primary); 124 131 } 125 132 126 133 .profile-tab.active { 127 - color: var(--text-primary); 134 + color: var(--text-primary); 128 135 } 129 136 130 137 .profile-tab.active::after { 131 - content: ""; 132 - position: absolute; 133 - bottom: -1px; 134 - left: 0; 135 - right: 0; 136 - height: 2px; 137 - background: var(--text-primary); 138 + content: ""; 139 + position: absolute; 140 + bottom: -1px; 141 + left: 0; 142 + right: 0; 143 + height: 2px; 144 + background: var(--text-primary); 138 145 } 139 146 140 147 .profile-badge-wrapper { 141 - display: inline-flex; 142 - align-items: center; 148 + display: inline-flex; 149 + align-items: center; 143 150 } 144 151 145 152 .profile-badge-clickable { 146 - position: relative; 147 - display: inline-flex; 148 - align-items: center; 149 - cursor: pointer; 150 - margin-left: 8px; 153 + position: relative; 154 + display: inline-flex; 155 + align-items: center; 156 + cursor: pointer; 157 + margin-left: 8px; 151 158 } 152 159 153 160 .badge-info-popover { 154 - position: absolute; 155 - top: calc(100% + 8px); 156 - left: 50%; 157 - transform: translateX(-50%); 158 - padding: 16px; 159 - background: var(--bg-elevated); 160 - border: 1px solid var(--border); 161 - border-radius: var(--radius-md); 162 - box-shadow: var(--shadow-lg); 163 - font-size: 0.85rem; 164 - white-space: nowrap; 165 - z-index: 100; 166 - min-width: 200px; 161 + position: absolute; 162 + top: calc(100% + 8px); 163 + left: 50%; 164 + transform: translateX(-50%); 165 + padding: 16px; 166 + background: var(--bg-elevated); 167 + border: 1px solid var(--border); 168 + border-radius: var(--radius-md); 169 + box-shadow: var(--shadow-lg); 170 + font-size: 0.85rem; 171 + white-space: nowrap; 172 + z-index: 100; 173 + min-width: 200px; 167 174 } 168 175 169 176 .badge-info-title { 170 - font-weight: 600; 171 - color: var(--text-primary); 172 - margin-bottom: 8px; 177 + font-weight: 600; 178 + color: var(--text-primary); 179 + margin-bottom: 8px; 173 180 } 174 181 175 182 .verifier-link { 176 - display: flex; 177 - align-items: center; 178 - gap: 8px; 179 - padding: 8px; 180 - background: var(--bg-tertiary); 181 - border-radius: var(--radius-sm); 182 - text-decoration: none; 183 - transition: background 0.15s ease; 183 + display: flex; 184 + align-items: center; 185 + gap: 8px; 186 + padding: 8px; 187 + background: var(--bg-tertiary); 188 + border-radius: var(--radius-sm); 189 + text-decoration: none; 190 + transition: background 0.15s ease; 184 191 } 185 192 186 193 .verifier-link:hover { 187 - background: var(--bg-hover); 194 + background: var(--bg-hover); 188 195 } 189 196 190 197 .verifier-avatar { 191 - width: 24px; 192 - height: 24px; 193 - border-radius: 50%; 194 - object-fit: cover; 198 + width: 24px; 199 + height: 24px; 200 + border-radius: 50%; 201 + object-fit: cover; 195 202 } 196 203 197 204 .verifier-name { 198 - color: var(--text-primary); 199 - font-size: 0.85rem; 200 - font-weight: 500; 205 + color: var(--text-primary); 206 + font-size: 0.85rem; 207 + font-weight: 500; 201 208 } 202 209 203 210 .profile-suspended { 204 - display: flex; 205 - flex-direction: column; 206 - align-items: center; 207 - justify-content: center; 208 - padding: 60px 20px; 209 - text-align: center; 210 - background: var(--bg-secondary); 211 - border-radius: var(--radius-lg); 212 - margin-top: 20px; 213 - border: 1px solid var(--border); 211 + display: flex; 212 + flex-direction: column; 213 + align-items: center; 214 + justify-content: center; 215 + padding: 60px 20px; 216 + text-align: center; 217 + background: var(--bg-secondary); 218 + border-radius: var(--radius-lg); 219 + margin-top: 20px; 220 + border: 1px solid var(--border); 214 221 } 215 222 216 223 .suspended-icon { 217 - font-size: 40px; 218 - margin-bottom: 16px; 219 - color: var(--text-tertiary); 224 + font-size: 40px; 225 + margin-bottom: 16px; 226 + color: var(--text-tertiary); 220 227 } 221 228 222 229 .profile-suspended h2 { 223 - color: var(--text-primary); 224 - margin-bottom: 8px; 225 - font-size: 1.25rem; 230 + color: var(--text-primary); 231 + margin-bottom: 8px; 232 + font-size: 1.25rem; 226 233 } 227 234 228 235 @media (max-width: 640px) { 229 - .profile-header { 230 - flex-direction: column; 231 - text-align: center; 232 - } 236 + .profile-header { 237 + flex-direction: column; 238 + text-align: center; 239 + } 233 240 234 - .profile-info { 235 - align-items: center; 236 - } 241 + .profile-info { 242 + align-items: center; 243 + } 237 244 238 - .profile-handle-row { 239 - justify-content: center; 240 - } 245 + .profile-handle-row { 246 + justify-content: center; 247 + } 241 248 242 - .profile-stats { 243 - justify-content: center; 244 - } 249 + .profile-stats { 250 + justify-content: center; 251 + } 245 252 246 - .profile-tabs { 247 - justify-content: center; 248 - gap: 16px; 249 - } 250 - } 253 + .profile-tabs { 254 + justify-content: center; 255 + gap: 16px; 256 + } 257 + }
+19
web/src/css/utilities.css
··· 261 261 margin-bottom: 16px; 262 262 font-style: italic; 263 263 color: var(--text-secondary); 264 + overflow-wrap: break-word; 265 + word-break: break-word; 266 + max-width: 100%; 264 267 } 265 268 266 269 .composer-quote-remove { ··· 728 731 .bookmark-time { 729 732 color: var(--text-tertiary); 730 733 } 734 + 735 + .bookmark-preview { 736 + max-width: 100%; 737 + width: 100%; 738 + box-sizing: border-box; 739 + } 740 + 741 + @media (max-width: 600px) { 742 + .bookmark-preview-content { 743 + padding: 12px 14px; 744 + } 745 + 746 + .legal-content { 747 + padding: 16px; 748 + } 749 + }
+7 -7
web/src/pages/Bookmarks.jsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 2 import { Link } from "react-router-dom"; 3 3 import { Plus } from "lucide-react"; 4 4 import { useAuth } from "../context/AuthContext"; ··· 22 22 const [submitting, setSubmitting] = useState(false); 23 23 const [fetchingTitle, setFetchingTitle] = useState(false); 24 24 25 - const loadBookmarks = async () => { 25 + const loadBookmarks = useCallback(async () => { 26 26 if (!user?.did) return; 27 27 28 28 try { ··· 35 35 } finally { 36 36 setLoadingBookmarks(false); 37 37 } 38 - }; 38 + }, [user]); 39 39 40 40 useEffect(() => { 41 41 if (isAuthenticated && user) { 42 42 loadBookmarks(); 43 43 } 44 - }, [isAuthenticated, user]); 44 + }, [isAuthenticated, user, loadBookmarks]); 45 45 46 46 const handleDelete = async (uri) => { 47 47 if (!confirm("Delete this bookmark?")) return; ··· 133 133 > 134 134 <div> 135 135 <h1 className="page-title">My Bookmarks</h1> 136 - <p className="page-description">Pages you've saved for later</p> 136 + <p className="page-description">Pages you&apos;ve saved for later</p> 137 137 </div> 138 138 <button 139 139 onClick={() => setShowAddForm(!showAddForm)} ··· 274 274 </div> 275 275 <h3 className="empty-state-title">No bookmarks yet</h3> 276 276 <p className="empty-state-text"> 277 - Click "Add Bookmark" above to save a page, or use the browser 278 - extension. 277 + Click &quot;Add Bookmark&quot; above to save a page, or use the 278 + browser extension. 279 279 </p> 280 280 </div> 281 281 ) : (
+4 -4
web/src/pages/CollectionDetail.jsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 2 import { useParams, useNavigate, Link, useLocation } from "react-router-dom"; 3 3 import { ArrowLeft, Edit2, Trash2, Plus } from "lucide-react"; 4 4 import { ··· 34 34 user?.did && 35 35 (collection?.creator?.did === user.did || paramAuthorDid === user.did); 36 36 37 - const fetchContext = async () => { 37 + const fetchContext = useCallback(async () => { 38 38 try { 39 39 setLoading(true); 40 40 ··· 96 96 } finally { 97 97 setLoading(false); 98 98 } 99 - }; 99 + }, [paramAuthorDid, user, handle, rkey, wildcardPath]); 100 100 101 101 useEffect(() => { 102 102 fetchContext(); 103 - }, [rkey, wildcardPath, handle, paramAuthorDid, user?.did]); 103 + }, [fetchContext]); 104 104 105 105 const handleEditSuccess = () => { 106 106 fetchContext();
+5 -6
web/src/pages/Collections.jsx
··· 1 - import { useState, useEffect } from "react"; 2 - import { Link } from "react-router-dom"; 3 - import { Folder, Plus, Edit2, ChevronRight } from "lucide-react"; 1 + import { useState, useEffect, useCallback } from "react"; 2 + import { Folder, Plus } from "lucide-react"; 4 3 import { getCollections } from "../api/client"; 5 4 import { useAuth } from "../context/AuthContext"; 6 5 import CollectionModal from "../components/CollectionModal"; ··· 14 13 const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); 15 14 const [editingCollection, setEditingCollection] = useState(null); 16 15 17 - const fetchCollections = async () => { 16 + const fetchCollections = useCallback(async () => { 18 17 try { 19 18 setLoading(true); 20 19 const data = await getCollections(user.did); ··· 25 24 } finally { 26 25 setLoading(false); 27 26 } 28 - }; 27 + }, [user]); 29 28 30 29 useEffect(() => { 31 30 if (user) { 32 31 fetchCollections(); 33 32 } 34 - }, [user]); 33 + }, [user, fetchCollections]); 35 34 36 35 const handleCreateSuccess = () => { 37 36 fetchCollections();
+1 -1
web/src/pages/Highlights.jsx
··· 77 77 <div className="page-header"> 78 78 <h1 className="page-title">My Highlights</h1> 79 79 <p className="page-description"> 80 - Text you've highlighted across the web 80 + Text you&apos;ve highlighted across the web 81 81 </p> 82 82 </div> 83 83
+99 -69
web/src/pages/Login.jsx
··· 2 2 import { Link } from "react-router-dom"; 3 3 import { useAuth } from "../context/AuthContext"; 4 4 import { searchActors, startLogin } from "../api/client"; 5 - import { HelpCircle } from "lucide-react"; 5 + import { AtSign } from "lucide-react"; 6 6 import logo from "../assets/logo.svg"; 7 7 8 8 export default function Login() { ··· 12 12 const [showInviteInput, setShowInviteInput] = useState(false); 13 13 const [suggestions, setSuggestions] = useState([]); 14 14 const [showSuggestions, setShowSuggestions] = useState(false); 15 - const [showHelp, setShowHelp] = useState(false); 16 15 const [loading, setLoading] = useState(false); 17 16 const [error, setError] = useState(null); 18 17 const [selectedIndex, setSelectedIndex] = useState(-1); ··· 20 19 const inviteRef = useRef(null); 21 20 const suggestionsRef = useRef(null); 22 21 23 - const isSelectionRef = useRef(false); 22 + const [providerIndex, setProviderIndex] = useState(0); 23 + const [morphClass, setMorphClass] = useState("morph-in"); 24 + const providers = [ 25 + "AT Protocol", 26 + "Bluesky", 27 + "Blacksky", 28 + "Tangled", 29 + "selfhosted.social", 30 + "Northsky", 31 + "witchcraft.systems", 32 + "topphie.social", 33 + "altq.net", 34 + ]; 24 35 25 36 useEffect(() => { 26 - if (handle.length < 3) { 27 - setSuggestions([]); 28 - setShowSuggestions(false); 29 - return; 30 - } 37 + const cycleText = () => { 38 + setMorphClass("morph-out"); 31 39 32 - if (isSelectionRef.current) { 33 - isSelectionRef.current = false; 34 - return; 35 - } 40 + setTimeout(() => { 41 + setProviderIndex((prev) => (prev + 1) % providers.length); 42 + setMorphClass("morph-in"); 43 + }, 400); 44 + }; 36 45 37 - const timer = setTimeout(async () => { 38 - try { 39 - const data = await searchActors(handle); 40 - setSuggestions(data.actors || []); 41 - setShowSuggestions(true); 42 - setSelectedIndex(-1); 43 - } catch (e) { 44 - console.error("Search failed:", e); 46 + const interval = setInterval(cycleText, 3000); 47 + return () => clearInterval(interval); 48 + }, [providers.length]); 49 + 50 + const isSelectionRef = useRef(false); 51 + 52 + useEffect(() => { 53 + if (handle.length >= 3) { 54 + if (isSelectionRef.current) { 55 + isSelectionRef.current = false; 56 + return; 45 57 } 46 - }, 300); 47 58 48 - return () => clearTimeout(timer); 59 + const timer = setTimeout(async () => { 60 + try { 61 + const data = await searchActors(handle); 62 + setSuggestions(data.actors || []); 63 + setShowSuggestions(true); 64 + setSelectedIndex(-1); 65 + } catch (e) { 66 + console.error("Search failed:", e); 67 + } 68 + }, 300); 69 + return () => clearTimeout(timer); 70 + } 49 71 }, [handle]); 50 72 51 73 useEffect(() => { ··· 63 85 return () => document.removeEventListener("mousedown", handleClickOutside); 64 86 }, []); 65 87 88 + if (isAuthenticated) { 89 + return ( 90 + <div className="login-page"> 91 + <div className="login-avatar-large"> 92 + {user?.avatar ? ( 93 + <img src={user.avatar} alt={user.displayName || user.handle} /> 94 + ) : ( 95 + <span> 96 + {(user?.displayName || user?.handle || "??") 97 + .substring(0, 2) 98 + .toUpperCase()} 99 + </span> 100 + )} 101 + </div> 102 + <h1 className="login-welcome"> 103 + Welcome back, {user?.displayName || user?.handle} 104 + </h1> 105 + <div className="login-actions"> 106 + <Link to={`/profile/${user?.did}`} className="btn btn-primary"> 107 + View Profile 108 + </Link> 109 + <button onClick={logout} className="btn btn-ghost"> 110 + Sign out 111 + </button> 112 + </div> 113 + </div> 114 + ); 115 + } 116 + 66 117 const handleKeyDown = (e) => { 67 118 if (!showSuggestions || suggestions.length === 0) return; 68 119 ··· 118 169 } 119 170 }; 120 171 121 - if (isAuthenticated) { 122 - return ( 123 - <div className="login-page"> 124 - <div className="login-avatar-large"> 125 - {user?.avatar ? ( 126 - <img src={user.avatar} alt={user.displayName || user.handle} /> 127 - ) : ( 128 - <span> 129 - {(user?.displayName || user?.handle || "??") 130 - .substring(0, 2) 131 - .toUpperCase()} 132 - </span> 133 - )} 134 - </div> 135 - <h1 className="login-welcome"> 136 - Welcome back, {user?.displayName || user?.handle} 137 - </h1> 138 - <div className="login-actions"> 139 - <Link to={`/profile/${user?.did}`} className="btn btn-primary"> 140 - View Profile 141 - </Link> 142 - <button onClick={logout} className="btn btn-ghost"> 143 - Sign out 144 - </button> 145 - </div> 146 - </div> 147 - ); 148 - } 149 - 150 172 return ( 151 173 <div className="login-page"> 152 - <img src={logo} alt="Margin Logo" className="login-logo-img" /> 174 + <div className="login-header-group"> 175 + <img src={logo} alt="Margin Logo" className="login-logo-img" /> 176 + <span className="login-x">X</span> 177 + <div className="login-atproto-icon"> 178 + <AtSign size={64} strokeWidth={2.4} /> 179 + </div> 180 + </div> 153 181 154 182 <h1 className="login-heading"> 155 - Use the AT Protocol to login to Margin 156 - <button 157 - className="login-help-btn" 158 - onClick={() => setShowHelp(!showHelp)} 159 - type="button" 160 - > 161 - <HelpCircle size={20} /> 162 - </button> 183 + Sign in with your{" "} 184 + <span className={`morph-container ${morphClass}`}> 185 + {providers[providerIndex]} 186 + </span>{" "} 187 + handle 163 188 </h1> 164 - 165 - {showHelp && ( 166 - <p className="login-help-text"> 167 - The AT Protocol is an open, decentralized network for social apps. 168 - Your handle looks like <code>name.bsky.social</code> or your own 169 - domain. 170 - </p> 171 - )} 172 189 173 190 <form onSubmit={handleSubmit} className="login-form"> 174 191 <div className="login-input-wrapper"> ··· 178 195 className="login-input" 179 196 placeholder="yourname.bsky.social" 180 197 value={handle} 181 - onChange={(e) => setHandle(e.target.value)} 198 + onChange={(e) => { 199 + const val = e.target.value; 200 + setHandle(val); 201 + if (val.length < 3) { 202 + setSuggestions([]); 203 + setShowSuggestions(false); 204 + } 205 + }} 182 206 onKeyDown={handleKeyDown} 183 207 onFocus={() => 184 208 handle.length >= 3 && ··· 261 285 ? "Submit Code" 262 286 : "Continue"} 263 287 </button> 288 + 289 + <p className="login-legal"> 290 + By signing in, you agree to our{" "} 291 + <Link to="/terms">Terms of Service</Link> and{" "} 292 + <Link to="/privacy">Privacy Policy</Link>. 293 + </p> 264 294 </form> 265 295 </div> 266 296 );
+2 -1
web/src/pages/Notifications.jsx
··· 156 156 <BellIcon size={48} /> 157 157 <h3>No notifications yet</h3> 158 158 <p> 159 - When someone likes or replies to your content, you'll see it here 159 + When someone likes or replies to your content, you&apos;ll see it 160 + here 160 161 </p> 161 162 </div> 162 163 )}
+7 -7
web/src/pages/Privacy.jsx
··· 16 16 <section> 17 17 <h2>Overview</h2> 18 18 <p> 19 - Margin ("we", "our", or "us") is a web annotation tool that lets you 20 - highlight, annotate, and bookmark any webpage. Your data is stored 21 - on the decentralized AT Protocol network, giving you ownership and 22 - control over your content. 19 + Margin (&quot;we&quot;, &quot;our&quot;, or &quot;us&quot;) is a web 20 + annotation tool that lets you highlight, annotate, and bookmark any 21 + webpage. Your data is stored on the decentralized AT Protocol 22 + network, giving you ownership and control over your content. 23 23 </p> 24 24 </section> 25 25 ··· 111 111 <strong>Cookies:</strong> To maintain your logged-in session 112 112 </li> 113 113 <li> 114 - <strong>Tabs:</strong> To know which page you're viewing 114 + <strong>Tabs:</strong> To know which page you&apos;re viewing 115 115 </li> 116 116 </ul> 117 117 </section> ··· 121 121 <p>You can:</p> 122 122 <ul> 123 123 <li> 124 - Delete any annotation, highlight, or bookmark you've created 124 + Delete any annotation, highlight, or bookmark you&apos;ve created 125 125 </li> 126 126 <li>Delete your collections</li> 127 127 <li>Export your data from your PDS</li> 128 - <li>Revoke the extension's access at any time</li> 128 + <li>Revoke the extension&apos;s access at any time</li> 129 129 </ul> 130 130 </section> 131 131
+250 -4
web/src/pages/Profile.jsx
··· 7 7 getUserHighlights, 8 8 getUserBookmarks, 9 9 getCollections, 10 + getAPIKeys, 11 + createAPIKey, 12 + deleteAPIKey, 10 13 } from "../api/client"; 14 + import { useAuth } from "../context/AuthContext"; 11 15 import CollectionIcon from "../components/CollectionIcon"; 12 16 import CollectionRow from "../components/CollectionRow"; 13 17 import { ··· 17 21 BlueskyIcon, 18 22 } from "../components/Icons"; 19 23 24 + function KeyIcon({ size = 16 }) { 25 + return ( 26 + <svg 27 + width={size} 28 + height={size} 29 + viewBox="0 0 24 24" 30 + fill="none" 31 + stroke="currentColor" 32 + strokeWidth="2" 33 + strokeLinecap="round" 34 + strokeLinejoin="round" 35 + > 36 + <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" /> 37 + </svg> 38 + ); 39 + } 40 + 20 41 export default function Profile() { 21 42 const { handle } = useParams(); 43 + const { user } = useAuth(); 22 44 const [activeTab, setActiveTab] = useState("annotations"); 23 45 const [profile, setProfile] = useState(null); 24 46 const [annotations, setAnnotations] = useState([]); 25 47 const [highlights, setHighlights] = useState([]); 26 48 const [bookmarks, setBookmarks] = useState([]); 27 49 const [collections, setCollections] = useState([]); 50 + const [apiKeys, setApiKeys] = useState([]); 51 + const [newKeyName, setNewKeyName] = useState(""); 52 + const [newKey, setNewKey] = useState(null); 53 + const [keysLoading, setKeysLoading] = useState(false); 28 54 const [loading, setLoading] = useState(true); 29 55 const [error, setError] = useState(null); 56 + 57 + const isOwnProfile = user && (user.did === handle || user.handle === handle); 30 58 31 59 useEffect(() => { 32 60 async function fetchProfile() { ··· 62 90 fetchProfile(); 63 91 }, [handle]); 64 92 93 + useEffect(() => { 94 + if (isOwnProfile && activeTab === "apikeys") { 95 + loadAPIKeys(); 96 + } 97 + }, [isOwnProfile, activeTab]); 98 + 99 + const loadAPIKeys = async () => { 100 + setKeysLoading(true); 101 + try { 102 + const data = await getAPIKeys(); 103 + setApiKeys(data.keys || []); 104 + } catch { 105 + setApiKeys([]); 106 + } finally { 107 + setKeysLoading(false); 108 + } 109 + }; 110 + 111 + const handleCreateKey = async () => { 112 + if (!newKeyName.trim()) return; 113 + try { 114 + const data = await createAPIKey(newKeyName.trim()); 115 + setNewKey(data.key); 116 + setNewKeyName(""); 117 + loadAPIKeys(); 118 + } catch (err) { 119 + alert("Failed to create key: " + err.message); 120 + } 121 + }; 122 + 123 + const handleDeleteKey = async (id) => { 124 + if (!confirm("Delete this API key? This cannot be undone.")) return; 125 + try { 126 + await deleteAPIKey(id); 127 + loadAPIKeys(); 128 + } catch (err) { 129 + alert("Failed to delete key: " + err.message); 130 + } 131 + }; 132 + 65 133 const displayName = profile?.displayName || profile?.handle || handle; 66 134 const displayHandle = 67 135 profile?.handle || (handle?.startsWith("did:") ? null : handle); ··· 89 157 </div> 90 158 <h3 className="empty-state-title">No annotations</h3> 91 159 <p className="empty-state-text"> 92 - This user hasn't posted any annotations. 160 + This user hasn&apos;t posted any annotations. 93 161 </p> 94 162 </div> 95 163 ); ··· 108 176 </div> 109 177 <h3 className="empty-state-title">No highlights</h3> 110 178 <p className="empty-state-text"> 111 - This user hasn't saved any highlights. 179 + This user hasn&apos;t saved any highlights. 112 180 </p> 113 181 </div> 114 182 ); ··· 125 193 </div> 126 194 <h3 className="empty-state-title">No bookmarks</h3> 127 195 <p className="empty-state-text"> 128 - This user hasn't bookmarked any pages. 196 + This user hasn&apos;t bookmarked any pages. 129 197 </p> 130 198 </div> 131 199 ); ··· 142 210 </div> 143 211 <h3 className="empty-state-title">No collections</h3> 144 212 <p className="empty-state-text"> 145 - This user hasn't created any collections. 213 + This user hasn&apos;t created any collections. 146 214 </p> 147 215 </div> 148 216 ); ··· 155 223 </div> 156 224 ); 157 225 } 226 + 227 + if (activeTab === "apikeys" && isOwnProfile) { 228 + return ( 229 + <div className="api-keys-section"> 230 + <div className="card" style={{ marginBottom: "1rem" }}> 231 + <h3 style={{ marginBottom: "0.5rem" }}>Create API Key</h3> 232 + <p 233 + style={{ 234 + color: "var(--text-muted)", 235 + marginBottom: "1rem", 236 + fontSize: "0.875rem", 237 + }} 238 + > 239 + Use API keys to create bookmarks from iOS Shortcuts or other 240 + tools. 241 + </p> 242 + <div style={{ display: "flex", gap: "0.5rem" }}> 243 + <input 244 + type="text" 245 + value={newKeyName} 246 + onChange={(e) => setNewKeyName(e.target.value)} 247 + placeholder="Key name (e.g., iOS Shortcut)" 248 + className="input" 249 + style={{ flex: 1 }} 250 + /> 251 + <button className="btn btn-primary" onClick={handleCreateKey}> 252 + Generate 253 + </button> 254 + </div> 255 + {newKey && ( 256 + <div 257 + style={{ 258 + marginTop: "1rem", 259 + padding: "1rem", 260 + background: "var(--bg-secondary)", 261 + borderRadius: "8px", 262 + }} 263 + > 264 + <p 265 + style={{ 266 + color: "var(--text-success)", 267 + fontWeight: 500, 268 + marginBottom: "0.5rem", 269 + }} 270 + > 271 + โœ“ Key created! Copy it now, you won&apos;t see it again. 272 + </p> 273 + <code 274 + style={{ 275 + display: "block", 276 + padding: "0.75rem", 277 + background: "var(--bg-tertiary)", 278 + borderRadius: "4px", 279 + wordBreak: "break-all", 280 + fontSize: "0.8rem", 281 + }} 282 + > 283 + {newKey} 284 + </code> 285 + <button 286 + className="btn btn-secondary" 287 + style={{ marginTop: "0.5rem" }} 288 + onClick={() => { 289 + navigator.clipboard.writeText(newKey); 290 + alert("Copied!"); 291 + }} 292 + > 293 + Copy to clipboard 294 + </button> 295 + </div> 296 + )} 297 + </div> 298 + 299 + {keysLoading ? ( 300 + <div className="card"> 301 + <div className="skeleton skeleton-text" /> 302 + </div> 303 + ) : apiKeys.length === 0 ? ( 304 + <div className="empty-state"> 305 + <div className="empty-state-icon"> 306 + <KeyIcon size={32} /> 307 + </div> 308 + <h3 className="empty-state-title">No API keys</h3> 309 + <p className="empty-state-text"> 310 + Create a key to use with iOS Shortcuts. 311 + </p> 312 + </div> 313 + ) : ( 314 + <div className="card"> 315 + <h3 style={{ marginBottom: "1rem" }}>Your API Keys</h3> 316 + {apiKeys.map((key) => ( 317 + <div 318 + key={key.id} 319 + style={{ 320 + display: "flex", 321 + justifyContent: "space-between", 322 + alignItems: "center", 323 + padding: "0.75rem 0", 324 + borderBottom: "1px solid var(--border-color)", 325 + }} 326 + > 327 + <div> 328 + <strong>{key.name}</strong> 329 + <div 330 + style={{ 331 + fontSize: "0.75rem", 332 + color: "var(--text-muted)", 333 + }} 334 + > 335 + Created {new Date(key.createdAt).toLocaleDateString()} 336 + {key.lastUsedAt && 337 + ` โ€ข Last used ${new Date(key.lastUsedAt).toLocaleDateString()}`} 338 + </div> 339 + </div> 340 + <button 341 + className="btn btn-sm" 342 + style={{ 343 + fontSize: "0.75rem", 344 + padding: "0.25rem 0.5rem", 345 + color: "#ef4444", 346 + border: "1px solid #ef4444", 347 + }} 348 + onClick={() => handleDeleteKey(key.id)} 349 + > 350 + Revoke 351 + </button> 352 + </div> 353 + ))} 354 + </div> 355 + )} 356 + 357 + <div className="card" style={{ marginTop: "1rem" }}> 358 + <h3 style={{ marginBottom: "0.5rem" }}>iOS Shortcut</h3> 359 + <p 360 + style={{ 361 + color: "var(--text-muted)", 362 + marginBottom: "1rem", 363 + fontSize: "0.875rem", 364 + }} 365 + > 366 + Save bookmarks from Safari&apos;s share sheet. 367 + </p> 368 + <a 369 + href="#" 370 + className="btn btn-primary" 371 + style={{ 372 + display: "inline-flex", 373 + alignItems: "center", 374 + gap: "0.5rem", 375 + opacity: 0.5, 376 + pointerEvents: "none", 377 + cursor: "default", 378 + }} 379 + onClick={(e) => e.preventDefault()} 380 + > 381 + <AppleIcon size={16} /> Coming Soon 382 + </a> 383 + </div> 384 + </div> 385 + ); 386 + } 158 387 }; 159 388 160 389 const bskyProfileUrl = displayHandle ··· 230 459 > 231 460 Collections ({collections.length}) 232 461 </button> 462 + 463 + {isOwnProfile && ( 464 + <button 465 + className={`profile-tab ${activeTab === "apikeys" ? "active" : ""}`} 466 + onClick={() => setActiveTab("apikeys")} 467 + > 468 + <KeyIcon size={14} /> API Keys 469 + </button> 470 + )} 233 471 </div> 234 472 235 473 {loading && ( ··· 262 500 </div> 263 501 ); 264 502 } 503 + 504 + function AppleIcon({ size = 16 }) { 505 + return ( 506 + <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"> 507 + <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" /> 508 + </svg> 509 + ); 510 + }
+7 -7
web/src/pages/Terms.jsx
··· 17 17 <h2>Overview</h2> 18 18 <p> 19 19 Margin is an open-source project. By using our service, you agree to 20 - these terms ("Terms"). If you do not agree to these Terms, please do 21 - not use the Service. 20 + these terms (&quot;Terms&quot;). If you do not agree to these Terms, 21 + please do not use the Service. 22 22 </p> 23 23 </section> 24 24 ··· 26 26 <h2>Open Source</h2> 27 27 <p> 28 28 Margin is open source software. The code is available publicly and 29 - is provided "as is", without warranty of any kind, express or 30 - implied. 29 + is provided &quot;as is&quot;, without warranty of any kind, express 30 + or implied. 31 31 </p> 32 32 </section> 33 33 ··· 62 62 <section> 63 63 <h2>Disclaimer</h2> 64 64 <p> 65 - THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE". WE DISCLAIM ALL 66 - CONDITIONS, REPRESENTATIONS AND WARRANTIES NOT EXPRESSLY SET OUT IN 67 - THESE TERMS. 65 + THE SERVICE IS PROVIDED &quot;AS IS&quot; AND &quot;AS 66 + AVAILABLE&quot;. WE DISCLAIM ALL CONDITIONS, REPRESENTATIONS AND 67 + WARRANTIES NOT EXPRESSLY SET OUT IN THESE TERMS. 68 68 </p> 69 69 </section> 70 70