A feed generator that allows Bluesky bookmarks via DMs

Oauth (#7)

authored by willdot.net and committed by GitHub ebc9ad7a be674d20

+1
.gitignore
··· 5 5 /tmp/ 6 6 7 7 .DS_Store 8 + *_templ.txt
+19 -156
auth_handlers.go
··· 1 1 package main 2 2 3 3 import ( 4 - "bytes" 5 - "encoding/json" 6 4 "fmt" 7 - "io" 8 5 "log/slog" 9 6 "net/http" 10 - "strconv" 11 - "time" 12 7 13 - "github.com/golang-jwt/jwt/v5" 14 8 "github.com/willdot/bskyfeedgen/frontend" 15 9 ) 16 10 17 11 const ( 18 - bskyBaseURL = "https://bsky.social/xrpc" 19 - jwtCookieName = "JWT" 20 - didCookieName = "DID" 12 + bskyBaseURL = "https://bsky.social/xrpc" 21 13 ) 22 14 23 15 type loginRequest struct { 24 - Handle string `json:"handle"` 25 - AppPassword string `json:"appPassword"` 26 - } 27 - 28 - type BskyAuth struct { 29 - AccessJwt string `json:"accessJwt"` 30 - Did string `json:"did"` 31 - } 32 - 33 - func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { 34 - b, err := io.ReadAll(r.Body) 35 - if err != nil { 36 - slog.Error("failed to read body", "error", err) 37 - _ = frontend.LoginForm("", "bad request").Render(r.Context(), w) 38 - return 39 - } 40 - 41 - var loginReq loginRequest 42 - err = json.Unmarshal(b, &loginReq) 43 - if err != nil { 44 - slog.Error("failed to unmarshal body", "error", err) 45 - _ = frontend.LoginForm("", "bad request").Render(r.Context(), w) 46 - return 47 - } 48 - url := fmt.Sprintf("%s/com.atproto.server.createsession", bskyBaseURL) 49 - 50 - requestData := map[string]interface{}{ 51 - "identifier": loginReq.Handle, 52 - "password": loginReq.AppPassword, 53 - } 54 - 55 - data, err := json.Marshal(requestData) 56 - if err != nil { 57 - slog.Error("failed marshal POST request to sign into Bsky", "error", err) 58 - _ = frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 59 - return 60 - } 61 - 62 - reader := bytes.NewReader(data) 63 - 64 - req, err := http.NewRequest("POST", url, reader) 65 - if err != nil { 66 - slog.Error("failed to create POST request to sign into Bsky", "error", err) 67 - _ = frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 68 - return 69 - } 70 - 71 - req.Header.Add("Content-Type", "application/json") 72 - 73 - // TODO: create a client somewhere 74 - res, err := http.DefaultClient.Do(req) 75 - if err != nil { 76 - slog.Error("failed to make POST request to sign into Bsky", "error", err) 77 - _ = frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 78 - return 79 - } 80 - 81 - defer res.Body.Close() 82 - 83 - slog.Info("bsky resp", "code", res.StatusCode) 84 - 85 - if res.StatusCode != 200 { 86 - slog.Error("failed to log into bluesky", "status code", res.StatusCode) 87 - _ = frontend.LoginForm(loginReq.Handle, "not authorized").Render(r.Context(), w) 88 - return 89 - } 90 - 91 - resBody, err := io.ReadAll(res.Body) 92 - if err != nil { 93 - slog.Error("failed read response from Bsky login", "error", err) 94 - _ = frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 95 - return 96 - } 97 - 98 - var loginResp BskyAuth 99 - err = json.Unmarshal(resBody, &loginResp) 100 - if err != nil { 101 - slog.Error("failed unmarshal response from Bsky login", "error", err) 102 - _ = frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 103 - return 104 - } 105 - 106 - http.SetCookie(w, &http.Cookie{ 107 - Name: jwtCookieName, 108 - Value: loginResp.AccessJwt, 109 - }) 110 - 111 - http.SetCookie(w, &http.Cookie{ 112 - Name: didCookieName, 113 - Value: loginResp.Did, 114 - }) 115 - 116 - http.Redirect(w, r, "/", http.StatusOK) 117 - } 118 - 119 - func (s *Server) HandleSignOut(w http.ResponseWriter, r *http.Request) { 120 - http.SetCookie(w, &http.Cookie{ 121 - Name: jwtCookieName, 122 - Value: "", 123 - }) 124 - 125 - http.SetCookie(w, &http.Cookie{ 126 - Name: didCookieName, 127 - Value: "", 128 - }) 129 - 130 - _ = frontend.Login("", "").Render(r.Context(), w) 16 + Handle string `json:"handle"` 131 17 } 132 18 133 19 func (s *Server) authMiddleware(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 134 20 return func(w http.ResponseWriter, r *http.Request) { 135 - jwtCookie, err := r.Cookie(jwtCookieName) 136 - if err != nil { 137 - slog.Error("read JWT cookie", "error", err) 138 - _ = frontend.Login("", "").Render(r.Context(), w) 139 - return 140 - } 141 - if jwtCookie == nil { 142 - slog.Error("missing JWT cookie") 21 + _, ok := s.getDidFromSession(r) 22 + if !ok { 23 + slog.Warn("did not found in session") 143 24 _ = frontend.Login("", "").Render(r.Context(), w) 144 25 return 145 26 } 146 27 147 - didCookie, err := r.Cookie(didCookieName) 148 - if err != nil { 149 - slog.Error("read DID cookie", "error", err) 150 - _ = frontend.Login("", "").Render(r.Context(), w) 151 - return 152 - } 153 - if didCookie == nil { 154 - slog.Error("missing DID cookie") 155 - _ = frontend.Login("", "").Render(r.Context(), w) 156 - return 157 - } 28 + next(w, r) 29 + } 30 + } 158 31 159 - claims := jwt.MapClaims{} 160 - _, _, err = jwt.NewParser().ParseUnverified(jwtCookie.Value, &claims) 161 - if err != nil { 162 - slog.Error("parsing JWT", "error", err) 163 - _ = frontend.Login("", "").Render(r.Context(), w) 164 - return 165 - } 32 + func (s *Server) getDidFromSession(r *http.Request) (string, bool) { 33 + session, err := s.sessionStore.Get(r, "oauth-session") 34 + if err != nil { 35 + slog.Error("getting session", "error", err) 36 + return "", false 37 + } 166 38 167 - if expiry, ok := claims["exp"].(string); ok { 168 - expiryInt, err := strconv.Atoi(expiry) 169 - if err != nil { 170 - slog.Error("invalid claims from token", "error", err) 171 - _ = frontend.Login("", "").Render(r.Context(), w) 172 - return 173 - } 39 + did, ok := session.Values["did"] 40 + if !ok { 41 + return "", false 42 + } 174 43 175 - if time.Now().Unix() > int64(expiryInt) { 176 - _ = frontend.Login("", "").Render(r.Context(), w) 177 - return 178 - } 179 - } 180 - next(w, r) 181 - } 44 + return fmt.Sprintf("%s", did), true 182 45 }
+9 -9
bookmark_handler.go
··· 17 17 ) 18 18 19 19 func (s *Server) HandleAddBookmark(w http.ResponseWriter, r *http.Request) { 20 - usersDid, err := getUsersDidFromRequestCookie(r) 21 - if err != nil { 22 - slog.Error("getting users did from request", "error", err) 20 + usersDid, ok := s.getDidFromSession(r) 21 + if !ok { 22 + slog.Warn("did not found in session") 23 23 _ = frontend.Login("", "").Render(r.Context(), w) 24 24 return 25 25 } ··· 115 115 func (s *Server) HandleDeleteBookmark(w http.ResponseWriter, r *http.Request) { 116 116 rKey := r.PathValue("rkey") 117 117 118 - usersDid, err := getUsersDidFromRequestCookie(r) 119 - if err != nil { 120 - slog.Error("getting users did from request", "error", err) 118 + usersDid, ok := s.getDidFromSession(r) 119 + if !ok { 120 + slog.Warn("did not found in session") 121 121 _ = frontend.Login("", "").Render(r.Context(), w) 122 122 return 123 123 } ··· 149 149 } 150 150 151 151 func (s *Server) HandleGetBookmarks(w http.ResponseWriter, r *http.Request) { 152 - usersDid, err := getUsersDidFromRequestCookie(r) 153 - if err != nil { 154 - slog.Error("getting users did from request", "error", err) 152 + usersDid, ok := s.getDidFromSession(r) 153 + if !ok { 154 + slog.Warn("did not found in session") 155 155 _ = frontend.Login("", "").Render(r.Context(), w) 156 156 return 157 157 }
+4 -4
frontend/base_templ.go
··· 1 1 // Code generated by templ - DO NOT EDIT. 2 2 3 - // templ: version: v0.2.793 3 + // templ: version: v0.3.833 4 4 package frontend 5 5 6 6 //lint:file-ignore SA4006 This context is only used if a nested component is present. ··· 29 29 templ_7745c5c3_Var1 = templ.NopComponent 30 30 } 31 31 ctx = templ.ClearChildren(ctx) 32 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 32 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><title>BSFeeder</title><link rel=\"icon\" type=\"image/x-icon\" href=\"/public/favicon.ico\"><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link href=\"/public/styles.css\" rel=\"stylesheet\"><script defer src=\"https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js\"></script><script src=\"https://unpkg.com/htmx.org\"></script><script src=\"https://unpkg.com/htmx.org@1.9.11\" defer></script><script src=\"https://unpkg.com/htmx.org@1.9.12/dist/ext/json-enc.js\"></script><script src=\"https://unpkg.com/htmx.org@1.9.11/dist/ext/response-targets.js\"></script></head><body hx-ext=\"response-targets\" class=\"antialiased\">") 33 33 if templ_7745c5c3_Err != nil { 34 34 return templ_7745c5c3_Err 35 35 } ··· 41 41 if templ_7745c5c3_Err != nil { 42 42 return templ_7745c5c3_Err 43 43 } 44 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 44 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</body></html>") 45 45 if templ_7745c5c3_Err != nil { 46 46 return templ_7745c5c3_Err 47 47 } 48 - return templ_7745c5c3_Err 48 + return nil 49 49 }) 50 50 } 51 51
-2
frontend/base_templ.txt
··· 1 - <!doctype html><html lang=\"en\"><head><title>BSFeeder</title><link rel=\"icon\" type=\"image/x-icon\" href=\"/public/favicon.ico\"><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link href=\"/public/styles.css\" rel=\"stylesheet\"><script defer src=\"https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js\"></script><script src=\"https://unpkg.com/htmx.org\"></script><script src=\"https://unpkg.com/htmx.org@1.9.11\" defer></script><script src=\"https://unpkg.com/htmx.org@1.9.12/dist/ext/json-enc.js\"></script><script src=\"https://unpkg.com/htmx.org@1.9.11/dist/ext/response-targets.js\"></script></head><body hx-ext=\"response-targets\" class=\"antialiased\"> 2 - </body></html>
+15 -15
frontend/bookmarks_templ.go
··· 1 1 // Code generated by templ - DO NOT EDIT. 2 2 3 - // templ: version: v0.2.793 3 + // templ: version: v0.3.833 4 4 package frontend 5 5 6 6 //lint:file-ignore SA4006 This context is only used if a nested component is present. ··· 38 38 if templ_7745c5c3_Err != nil { 39 39 return templ_7745c5c3_Err 40 40 } 41 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 41 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div hx-ext=\"response-targets\" class=\"flex justify-center items-center pt-6\"><form hx-post=\"/bookmarks\" hx-trigger=\"submit\" hx-target=\"#result\" hx-swap=\"innerHTML\" hx-target-error=\"#result\" class=\"w-96\" hx-on::after-request=\"this.reset()\"><input name=\"uri\" class=\"rounded-lg w-full mb-2 p-4\" placeholder=\"Add Post URI here\"> <button class=\"py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800\">Add Bookmark</button><div id=\"result\" class=\"text-red-500 font-bold items-center pt-6\"></div></form></div><div hx-ext=\"response-targets\" class=\"flex justify-center items-center pt-6\"><table class=\"min-w-half divide-y-2 divide-gray-200 bg-white text-sm\"><tbody class=\"divide-y divide-gray-200\" id=\"bookmarks-table\">") 42 42 if templ_7745c5c3_Err != nil { 43 43 return templ_7745c5c3_Err 44 44 } ··· 48 48 return templ_7745c5c3_Err 49 49 } 50 50 } 51 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 51 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</tbody></table></div>") 52 52 if templ_7745c5c3_Err != nil { 53 53 return templ_7745c5c3_Err 54 54 } 55 - return templ_7745c5c3_Err 55 + return nil 56 56 }) 57 57 } 58 58 ··· 77 77 templ_7745c5c3_Var2 = templ.NopComponent 78 78 } 79 79 ctx = templ.ClearChildren(ctx) 80 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 80 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<tr id=\"") 81 81 if templ_7745c5c3_Err != nil { 82 82 return templ_7745c5c3_Err 83 83 } ··· 90 90 if templ_7745c5c3_Err != nil { 91 91 return templ_7745c5c3_Err 92 92 } 93 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 93 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><td class=\"px-4 py-2 font-medium text-gray-900\"><p class=\"font-medium text-sm text-blue-300\">Author: ") 94 94 if templ_7745c5c3_Err != nil { 95 95 return templ_7745c5c3_Err 96 96 } ··· 103 103 if templ_7745c5c3_Err != nil { 104 104 return templ_7745c5c3_Err 105 105 } 106 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) 106 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p><a class=\"font-medium text-sm\" target=\"_blank\" href=\"") 107 107 if templ_7745c5c3_Err != nil { 108 108 return templ_7745c5c3_Err 109 109 } ··· 112 112 if templ_7745c5c3_Err != nil { 113 113 return templ_7745c5c3_Err 114 114 } 115 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) 115 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">") 116 116 if templ_7745c5c3_Err != nil { 117 117 return templ_7745c5c3_Err 118 118 } ··· 125 125 if templ_7745c5c3_Err != nil { 126 126 return templ_7745c5c3_Err 127 127 } 128 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 7) 128 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a></td><td class=\"whitespace-nowrap px-4 py-2 text-gray-700\"><button hx-delete=\"") 129 129 if templ_7745c5c3_Err != nil { 130 130 return templ_7745c5c3_Err 131 131 } ··· 138 138 if templ_7745c5c3_Err != nil { 139 139 return templ_7745c5c3_Err 140 140 } 141 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 8) 141 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-swap=\"delete\" hx-target=\"") 142 142 if templ_7745c5c3_Err != nil { 143 143 return templ_7745c5c3_Err 144 144 } ··· 151 151 if templ_7745c5c3_Err != nil { 152 152 return templ_7745c5c3_Err 153 153 } 154 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 9) 154 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"flex items-center border py-1 px-2 rounded-lg hover:bg-red-300\"><p class=\"text-sm\">Delete</p></button></td></tr>") 155 155 if templ_7745c5c3_Err != nil { 156 156 return templ_7745c5c3_Err 157 157 } 158 - return templ_7745c5c3_Err 158 + return nil 159 159 }) 160 160 } 161 161 ··· 180 180 templ_7745c5c3_Var9 = templ.NopComponent 181 181 } 182 182 ctx = templ.ClearChildren(ctx) 183 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 10) 183 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<tbody hx-swap-oob=\"beforeend:#bookmarks-table\">") 184 184 if templ_7745c5c3_Err != nil { 185 185 return templ_7745c5c3_Err 186 186 } ··· 188 188 if templ_7745c5c3_Err != nil { 189 189 return templ_7745c5c3_Err 190 190 } 191 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 11) 191 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody>") 192 192 if templ_7745c5c3_Err != nil { 193 193 return templ_7745c5c3_Err 194 194 } 195 - return templ_7745c5c3_Err 195 + return nil 196 196 }) 197 197 } 198 198
-11
frontend/bookmarks_templ.txt
··· 1 - <div hx-ext=\"response-targets\" class=\"flex justify-center items-center pt-6\"><form hx-post=\"/bookmarks\" hx-trigger=\"submit\" hx-target=\"#result\" hx-swap=\"innerHTML\" hx-target-error=\"#result\" class=\"w-96\" hx-on::after-request=\"this.reset()\"><input name=\"uri\" class=\"rounded-lg w-full mb-2 p-4\" placeholder=\"Add Post URI here\"> <button class=\"py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800\">Add Bookmark</button><div id=\"result\" class=\"text-red-500 font-bold items-center pt-6\"></div></form></div><div hx-ext=\"response-targets\" class=\"flex justify-center items-center pt-6\"><table class=\"min-w-half divide-y-2 divide-gray-200 bg-white text-sm\"><tbody class=\"divide-y divide-gray-200\" id=\"bookmarks-table\"> 2 - </tbody></table></div> 3 - <tr id=\" 4 - \"><td class=\"px-4 py-2 font-medium text-gray-900\"><p class=\"font-medium text-sm text-blue-300\">Author: 5 - </p><a class=\"font-medium text-sm\" target=\"_blank\" href=\" 6 - \"> 7 - </a></td><td class=\"whitespace-nowrap px-4 py-2 text-gray-700\"><button hx-delete=\" 8 - \" hx-swap=\"delete\" hx-target=\" 9 - \" class=\"flex items-center border py-1 px-2 rounded-lg hover:bg-red-300\"><p class=\"text-sm\">Delete</p></button></td></tr> 10 - <tbody hx-swap-oob=\"beforeend:#bookmarks-table\"> 11 - </tbody>
+11 -11
frontend/home_templ.go
··· 1 1 // Code generated by templ - DO NOT EDIT. 2 2 3 - // templ: version: v0.2.793 3 + // templ: version: v0.3.833 4 4 package frontend 5 5 6 6 //lint:file-ignore SA4006 This context is only used if a nested component is present. ··· 33 33 if templ_7745c5c3_Err != nil { 34 34 return templ_7745c5c3_Err 35 35 } 36 - return templ_7745c5c3_Err 36 + return nil 37 37 }) 38 38 } 39 39 ··· 62 62 if templ_7745c5c3_Err != nil { 63 63 return templ_7745c5c3_Err 64 64 } 65 - return templ_7745c5c3_Err 65 + return nil 66 66 }) 67 67 } 68 68 ··· 91 91 if templ_7745c5c3_Err != nil { 92 92 return templ_7745c5c3_Err 93 93 } 94 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 94 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"relative flex justify-center overflow-hidden bg-gray-50 py-6 sm:py-12\"><div class=\"flex-1 mx-auto w-full max-w-md bg-white px-6 pt-6 pb-6 shadow-xl ring-1 ring-gray-900/5 sm:rounded-xl sm:px-8\"><div class=\"w-full\"><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your username is</h1><p class=\"mt-2 text-gray-500\">") 95 95 if templ_7745c5c3_Err != nil { 96 96 return templ_7745c5c3_Err 97 97 } ··· 104 104 if templ_7745c5c3_Err != nil { 105 105 return templ_7745c5c3_Err 106 106 } 107 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 107 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</p></div><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your email is</h1><p class=\"mt-2 text-gray-500\">") 108 108 if templ_7745c5c3_Err != nil { 109 109 return templ_7745c5c3_Err 110 110 } ··· 117 117 if templ_7745c5c3_Err != nil { 118 118 return templ_7745c5c3_Err 119 119 } 120 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 120 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p></div></div></div><div class=\"flex-1 mx-auto w-full max-w-md bg-white px-6 pt-6 pb-6 shadow-xl ring-1 ring-gray-900/5 sm:rounded-xl sm:px-8\"><div class=\"w-full\"><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your username is</h1><p class=\"mt-2 text-gray-500\">") 121 121 if templ_7745c5c3_Err != nil { 122 122 return templ_7745c5c3_Err 123 123 } ··· 130 130 if templ_7745c5c3_Err != nil { 131 131 return templ_7745c5c3_Err 132 132 } 133 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 133 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p></div><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your email is</h1><p class=\"mt-2 text-gray-500\">") 134 134 if templ_7745c5c3_Err != nil { 135 135 return templ_7745c5c3_Err 136 136 } ··· 143 143 if templ_7745c5c3_Err != nil { 144 144 return templ_7745c5c3_Err 145 145 } 146 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) 146 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div></div></div><div class=\"flex-1 mx-auto w-full max-w-md bg-white px-6 pt-6 pb-6 shadow-xl ring-1 ring-gray-900/5 sm:rounded-xl sm:px-8\"><div class=\"w-full\"><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your username is</h1><p class=\"mt-2 text-gray-500\">") 147 147 if templ_7745c5c3_Err != nil { 148 148 return templ_7745c5c3_Err 149 149 } ··· 156 156 if templ_7745c5c3_Err != nil { 157 157 return templ_7745c5c3_Err 158 158 } 159 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) 159 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p></div><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your email is</h1><p class=\"mt-2 text-gray-500\">") 160 160 if templ_7745c5c3_Err != nil { 161 161 return templ_7745c5c3_Err 162 162 } ··· 169 169 if templ_7745c5c3_Err != nil { 170 170 return templ_7745c5c3_Err 171 171 } 172 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 7) 172 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div></div></div></div>") 173 173 if templ_7745c5c3_Err != nil { 174 174 return templ_7745c5c3_Err 175 175 } 176 - return templ_7745c5c3_Err 176 + return nil 177 177 }) 178 178 } 179 179
-7
frontend/home_templ.txt
··· 1 - <div class=\"relative flex justify-center overflow-hidden bg-gray-50 py-6 sm:py-12\"><div class=\"flex-1 mx-auto w-full max-w-md bg-white px-6 pt-6 pb-6 shadow-xl ring-1 ring-gray-900/5 sm:rounded-xl sm:px-8\"><div class=\"w-full\"><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your username is</h1><p class=\"mt-2 text-gray-500\"> 2 - </p></div><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your email is</h1><p class=\"mt-2 text-gray-500\"> 3 - </p></div></div></div><div class=\"flex-1 mx-auto w-full max-w-md bg-white px-6 pt-6 pb-6 shadow-xl ring-1 ring-gray-900/5 sm:rounded-xl sm:px-8\"><div class=\"w-full\"><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your username is</h1><p class=\"mt-2 text-gray-500\"> 4 - </p></div><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your email is</h1><p class=\"mt-2 text-gray-500\"> 5 - </p></div></div></div><div class=\"flex-1 mx-auto w-full max-w-md bg-white px-6 pt-6 pb-6 shadow-xl ring-1 ring-gray-900/5 sm:rounded-xl sm:px-8\"><div class=\"w-full\"><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your username is</h1><p class=\"mt-2 text-gray-500\"> 6 - </p></div><div class=\"text-center\"><h1 class=\"text-3xl font-semibold text-gray-900\">Your email is</h1><p class=\"mt-2 text-gray-500\"> 7 - </p></div></div></div></div>
-10
frontend/login.templ
··· 18 18 <input class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500" id="handle" name="handle" type="text" value={ handle }/> 19 19 </div> 20 20 </div> 21 - <div class="md:flex md:items-center mb-6"> 22 - <div class="md:w-1/3"> 23 - <label class="block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4" for="appPassword"> 24 - App Password 25 - </label> 26 - </div> 27 - <div class="md:w-2/3"> 28 - <input class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500" id="appPassword" name="appPassword" type="password"/> 29 - </div> 30 - </div> 31 21 <div class="md:flex md:items-center"> 32 22 <div class="md:w-1/3"></div> 33 23 <div class="md:w-1/3">
+9 -9
frontend/login_templ.go
··· 1 1 // Code generated by templ - DO NOT EDIT. 2 2 3 - // templ: version: v0.2.793 3 + // templ: version: v0.3.833 4 4 package frontend 5 5 6 6 //lint:file-ignore SA4006 This context is only used if a nested component is present. ··· 37 37 if templ_7745c5c3_Err != nil { 38 38 return templ_7745c5c3_Err 39 39 } 40 - return templ_7745c5c3_Err 40 + return nil 41 41 }) 42 42 } 43 43 ··· 62 62 templ_7745c5c3_Var2 = templ.NopComponent 63 63 } 64 64 ctx = templ.ClearChildren(ctx) 65 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 65 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form class=\"h-screen flex items-center justify-center\" id=\"login-form\" hx-swap=\"outerHTML\" hx-post=\"/login\" hx-ext=\"json-enc\"><div class=\"w-full max-w-sm\"><div class=\"md:flex md:items-center mb-6\"><div class=\"md:w-1/3\"><label class=\"block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4\" for=\"handle\">Bsky Handle</label></div><div class=\"md:w-2/3\"><input class=\"bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500\" id=\"handle\" name=\"handle\" type=\"text\" value=\"") 66 66 if templ_7745c5c3_Err != nil { 67 67 return templ_7745c5c3_Err 68 68 } ··· 75 75 if templ_7745c5c3_Err != nil { 76 76 return templ_7745c5c3_Err 77 77 } 78 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 78 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"></div></div><div class=\"md:flex md:items-center\"><div class=\"md:w-1/3\"></div><div class=\"md:w-1/3\"><button class=\"shadow bg-blue-500 hover:bg-blue-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded\" type=\"submit\" form=\"login-form\">Login</button></div>") 79 79 if templ_7745c5c3_Err != nil { 80 80 return templ_7745c5c3_Err 81 81 } 82 82 if errorMsg != "" { 83 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 83 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"md:w-1/3\" id=\"error-message\"><label class=\"text-red-500 font-bold\">") 84 84 if templ_7745c5c3_Err != nil { 85 85 return templ_7745c5c3_Err 86 86 } 87 87 var templ_7745c5c3_Var4 string 88 88 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg) 89 89 if templ_7745c5c3_Err != nil { 90 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `frontend/login.templ`, Line: 41, Col: 17} 90 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `frontend/login.templ`, Line: 31, Col: 17} 91 91 } 92 92 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 93 93 if templ_7745c5c3_Err != nil { 94 94 return templ_7745c5c3_Err 95 95 } 96 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 96 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</label></div>") 97 97 if templ_7745c5c3_Err != nil { 98 98 return templ_7745c5c3_Err 99 99 } 100 100 } 101 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) 101 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div></form>") 102 102 if templ_7745c5c3_Err != nil { 103 103 return templ_7745c5c3_Err 104 104 } 105 - return templ_7745c5c3_Err 105 + return nil 106 106 }) 107 107 } 108 108
-5
frontend/login_templ.txt
··· 1 - <form class=\"h-screen flex items-center justify-center\" id=\"login-form\" hx-swap=\"outerHTML\" hx-post=\"/login\" hx-ext=\"json-enc\"><div class=\"w-full max-w-sm\"><div class=\"md:flex md:items-center mb-6\"><div class=\"md:w-1/3\"><label class=\"block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4\" for=\"handle\">Bsky Handle</label></div><div class=\"md:w-2/3\"><input class=\"bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500\" id=\"handle\" name=\"handle\" type=\"text\" value=\" 2 - \"></div></div><div class=\"md:flex md:items-center mb-6\"><div class=\"md:w-1/3\"><label class=\"block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4\" for=\"appPassword\">App Password</label></div><div class=\"md:w-2/3\"><input class=\"bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-blue-500\" id=\"appPassword\" name=\"appPassword\" type=\"password\"></div></div><div class=\"md:flex md:items-center\"><div class=\"md:w-1/3\"></div><div class=\"md:w-1/3\"><button class=\"shadow bg-blue-500 hover:bg-blue-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded\" type=\"submit\" form=\"login-form\">Login</button></div> 3 - <div class=\"md:w-1/3\" id=\"error-message\"><label class=\"text-red-500 font-bold\"> 4 - </label></div> 5 - </div></div></form>
+3 -3
frontend/nav_templ.go
··· 1 1 // Code generated by templ - DO NOT EDIT. 2 2 3 - // templ: version: v0.2.793 3 + // templ: version: v0.3.833 4 4 package frontend 5 5 6 6 //lint:file-ignore SA4006 This context is only used if a nested component is present. ··· 31 31 templ_7745c5c3_Var1 = templ.NopComponent 32 32 } 33 33 ctx = templ.ClearChildren(ctx) 34 - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 34 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<header class=\"header sticky top-0 bg-white shadow-md flex items-center justify-between px-8 py-02\"><nav class=\"nav font-semibold text-lg\"><ul class=\"flex items-center\"><li class=\"p-4 text-blue-500 hover:text-blue-800\"><a href=\"/\">Home</a></li><li class=\"p-4 text-blue-500 hover:text-blue-800\"><a href=\"/bookmarks\">Bookmarks</a></li></ul></nav><div class=\"w-3/12 flex justify-end\"><div class=\"p-4 text-blue-500 hover:text-blue-800\"><a class=\"text-right\" href=\"/sign-out\">Sign Out </a></div></div></header>") 35 35 if templ_7745c5c3_Err != nil { 36 36 return templ_7745c5c3_Err 37 37 } 38 - return templ_7745c5c3_Err 38 + return nil 39 39 }) 40 40 } 41 41
-1
frontend/nav_templ.txt
··· 1 - <header class=\"header sticky top-0 bg-white shadow-md flex items-center justify-between px-8 py-02\"><nav class=\"nav font-semibold text-lg\"><ul class=\"flex items-center\"><li class=\"p-4 text-blue-500 hover:text-blue-800\"><a href=\"/\">Home</a></li><li class=\"p-4 text-blue-500 hover:text-blue-800\"><a href=\"/bookmarks\">Bookmarks</a></li></ul></nav><div class=\"w-3/12 flex justify-end\"><div class=\"p-4 text-blue-500 hover:text-blue-800\"><a class=\"text-right\" href=\"/sign-out\">Sign Out </a></div></div></header>
+35 -27
go.mod
··· 1 1 module github.com/willdot/bskyfeedgen 2 2 3 - go 1.23.3 3 + go 1.24.0 4 4 5 - toolchain go1.23.4 5 + toolchain go1.24.1 6 6 7 7 require ( 8 - github.com/a-h/templ v0.2.793 8 + github.com/a-h/templ v0.3.833 9 9 github.com/avast/retry-go/v4 v4.6.0 10 - github.com/bluesky-social/indigo v0.0.0-20241031232035-1a73c3fb6841 10 + github.com/bluesky-social/indigo v0.0.0-20250305203105-a2e0aaff387e 11 11 github.com/bluesky-social/jetstream v0.0.0-20241031234625-0ab10bd041fe 12 12 github.com/bugsnag/bugsnag-go/v2 v2.5.1 13 13 github.com/glebarez/go-sqlite v1.22.0 14 14 github.com/golang-jwt/jwt/v5 v5.2.1 15 + github.com/gorilla/sessions v1.4.0 16 + github.com/haileyok/atproto-oauth-golang v0.0.2 15 17 github.com/joho/godotenv v1.5.1 16 - github.com/stretchr/testify v1.9.0 18 + github.com/lestrrat-go/jwx/v2 v2.1.4 17 19 ) 18 20 19 21 require ( ··· 21 23 github.com/bugsnag/panicwrap v1.3.4 // indirect 22 24 github.com/carlmjohnson/versioninfo v0.22.5 // indirect 23 25 github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 - github.com/davecgh/go-spew v1.1.1 // indirect 26 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 25 27 github.com/dustin/go-humanize v1.0.1 // indirect 26 28 github.com/felixge/httpsnoop v1.0.4 // indirect 27 - github.com/go-logr/logr v1.4.1 // indirect 29 + github.com/go-logr/logr v1.4.2 // indirect 28 30 github.com/go-logr/stdr v1.2.2 // indirect 29 - github.com/goccy/go-json v0.10.2 // indirect 31 + github.com/goccy/go-json v0.10.5 // indirect 30 32 github.com/gogo/protobuf v1.3.2 // indirect 31 33 github.com/google/uuid v1.6.0 // indirect 34 + github.com/gorilla/securecookie v1.1.2 // indirect 32 35 github.com/gorilla/websocket v1.5.1 // indirect 33 36 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 34 - github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 37 + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 35 38 github.com/hashicorp/golang-lru v1.0.2 // indirect 36 39 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 37 40 github.com/ipfs/bbloom v0.0.4 // indirect 38 41 github.com/ipfs/go-block-format v0.2.0 // indirect 39 - github.com/ipfs/go-cid v0.4.1 // indirect 40 - github.com/ipfs/go-datastore v0.6.0 // indirect 42 + github.com/ipfs/go-cid v0.5.0 // indirect 43 + github.com/ipfs/go-datastore v0.8.2 // indirect 41 44 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 42 45 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 43 46 github.com/ipfs/go-ipfs-util v0.0.3 // indirect 44 - github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 47 + github.com/ipfs/go-ipld-cbor v0.2.0 // indirect 45 48 github.com/ipfs/go-ipld-format v0.6.0 // indirect 46 49 github.com/ipfs/go-log v1.0.5 // indirect 47 50 github.com/ipfs/go-log/v2 v2.5.1 // indirect 48 - github.com/ipfs/go-metrics-interface v0.0.1 // indirect 51 + github.com/ipfs/go-metrics-interface v0.3.0 // indirect 49 52 github.com/jbenet/goprocess v0.1.4 // indirect 50 53 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 51 54 github.com/klauspost/compress v1.17.9 // indirect 52 - github.com/klauspost/cpuid/v2 v2.2.7 // indirect 55 + github.com/klauspost/cpuid/v2 v2.2.10 // indirect 56 + github.com/lestrrat-go/blackmagic v1.0.2 // indirect 57 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 58 + github.com/lestrrat-go/httprc v1.0.6 // indirect 59 + github.com/lestrrat-go/iter v1.0.2 // indirect 60 + github.com/lestrrat-go/option v1.0.1 // indirect 53 61 github.com/mattn/go-isatty v0.0.20 // indirect 54 62 github.com/minio/sha256-simd v1.0.1 // indirect 55 63 github.com/mr-tron/base58 v1.2.0 // indirect ··· 60 68 github.com/multiformats/go-varint v0.0.7 // indirect 61 69 github.com/opentracing/opentracing-go v1.2.0 // indirect 62 70 github.com/pkg/errors v0.9.1 // indirect 63 - github.com/pmezard/go-difflib v1.0.0 // indirect 64 71 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 65 72 github.com/prometheus/client_golang v1.19.1 // indirect 66 73 github.com/prometheus/client_model v0.6.1 // indirect 67 74 github.com/prometheus/common v0.54.0 // indirect 68 75 github.com/prometheus/procfs v0.15.1 // indirect 69 76 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 77 + github.com/segmentio/asm v1.2.0 // indirect 70 78 github.com/spaolacci/murmur3 v1.1.0 // indirect 71 - github.com/whyrusleeping/cbor-gen v0.1.3-0.20240904181319-8dc02b38228c // indirect 79 + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect 72 80 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 73 81 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 74 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 75 - go.opentelemetry.io/otel v1.21.0 // indirect 76 - go.opentelemetry.io/otel/metric v1.21.0 // indirect 77 - go.opentelemetry.io/otel/trace v1.21.0 // indirect 82 + go.opentelemetry.io/auto/sdk v1.1.0 // indirect 83 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 84 + go.opentelemetry.io/otel v1.35.0 // indirect 85 + go.opentelemetry.io/otel/metric v1.35.0 // indirect 86 + go.opentelemetry.io/otel/trace v1.35.0 // indirect 78 87 go.uber.org/atomic v1.11.0 // indirect 79 88 go.uber.org/multierr v1.11.0 // indirect 80 89 go.uber.org/zap v1.27.0 // indirect 81 - golang.org/x/crypto v0.26.0 // indirect 82 - golang.org/x/net v0.28.0 // indirect 83 - golang.org/x/sys v0.23.0 // indirect 84 - golang.org/x/time v0.5.0 // indirect 85 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 90 + golang.org/x/crypto v0.36.0 // indirect 91 + golang.org/x/net v0.33.0 // indirect 92 + golang.org/x/sys v0.31.0 // indirect 93 + golang.org/x/time v0.8.0 // indirect 94 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 86 95 google.golang.org/protobuf v1.34.2 // indirect 87 - gopkg.in/yaml.v3 v3.0.1 // indirect 88 - lukechampine.com/blake3 v1.2.1 // indirect 96 + lukechampine.com/blake3 v1.4.0 // indirect 89 97 modernc.org/libc v1.37.6 // indirect 90 98 modernc.org/mathutil v1.6.0 // indirect 91 99 modernc.org/memory v1.7.2 // indirect
+135 -26
go.sum
··· 1 1 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 2 github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY= 3 3 github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= 4 + github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= 5 + github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= 4 6 github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= 5 7 github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= 6 8 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= ··· 8 10 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 11 github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= 10 12 github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= 11 - github.com/bluesky-social/indigo v0.0.0-20241031232035-1a73c3fb6841 h1:HCj4iBoAV59Fn1vsEhbeEMOExR4vOrUgz+sxUoV+F6s= 12 - github.com/bluesky-social/indigo v0.0.0-20241031232035-1a73c3fb6841/go.mod h1:Zx9nSWgd/FxMenkJW07VKnzspxpHBdPrPmS+Fspl2I0= 13 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 14 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 15 + github.com/bluesky-social/indigo v0.0.0-20250305203105-a2e0aaff387e h1:cLBQmPW9D6F7F3FRoFeu5yyHzWo77nSun3plvHLM7yM= 16 + github.com/bluesky-social/indigo v0.0.0-20250305203105-a2e0aaff387e/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 13 17 github.com/bluesky-social/jetstream v0.0.0-20241031234625-0ab10bd041fe h1:jduuyDfsiwWrPiN7psqDehepl68uxQ4UYCIgoqb1D4o= 14 18 github.com/bluesky-social/jetstream v0.0.0-20241031234625-0ab10bd041fe/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 15 19 github.com/bugsnag/bugsnag-go/v2 v2.5.1 h1:cGsEJHcis1zfQ4KoFaBPIT4N1TYqVNRALKr2wMRZ4hs= ··· 22 26 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 27 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 24 28 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 29 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 31 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 33 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 34 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 35 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 36 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 27 37 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 28 38 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 29 39 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= ··· 31 41 github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= 32 42 github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 33 43 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 34 - github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 35 - github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 44 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 45 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 36 46 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 37 47 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 38 48 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 39 49 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 40 50 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 51 + github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 52 + github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 41 53 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 42 54 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 55 + github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 43 56 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 44 57 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 45 58 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 46 59 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 60 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 61 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 62 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 63 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 48 64 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 49 65 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= ··· 51 67 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 68 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 53 69 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 70 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 71 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 72 + github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 73 + github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 54 74 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 55 75 github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 76 + github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= 77 + github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 56 78 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 57 79 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 58 80 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 59 81 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 82 + github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 60 83 github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 61 84 github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 85 + github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 86 + github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 62 87 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 63 88 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 64 89 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= ··· 69 94 github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 70 95 github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 71 96 github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 97 + github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 98 + github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 72 99 github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 73 100 github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 101 + github.com/ipfs/go-datastore v0.8.2 h1:Jy3wjqQR6sg/LhyY0NIePZC3Vux19nLtg7dx0TVqr6U= 102 + github.com/ipfs/go-datastore v0.8.2/go.mod h1:W+pI1NsUsz3tcsAACMtfC+IZdnQTnC/7VfPoJBQuts0= 74 103 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 75 104 github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 76 105 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= ··· 81 110 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 82 111 github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 83 112 github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 113 + github.com/ipfs/go-ipld-cbor v0.2.0 h1:VHIW3HVIjcMd8m4ZLZbrYpwjzqlVUfjLM7oK4T5/YF0= 114 + github.com/ipfs/go-ipld-cbor v0.2.0/go.mod h1:Cp8T7w1NKcu4AQJLqK0tWpd1nkgTxEVB5C6kVpLW6/0= 84 115 github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 85 116 github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 86 117 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= ··· 90 121 github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 91 122 github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 92 123 github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 124 + github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 125 + github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 93 126 github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 94 127 github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 95 128 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= ··· 105 138 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 106 139 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 107 140 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 141 + github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 142 + github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 108 143 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 109 144 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 110 145 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= ··· 112 147 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 113 148 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 114 149 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 150 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 151 + github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 152 + github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 153 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 154 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 155 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 156 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 157 + github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= 158 + github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 159 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 160 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 161 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 162 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 163 + github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc= 164 + github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= 165 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 166 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 167 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 115 168 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 116 169 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 117 170 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= ··· 134 187 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 135 188 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 136 189 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 137 - github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 138 190 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 191 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 192 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 139 193 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 140 194 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 141 195 github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= ··· 149 203 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 150 204 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 151 205 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 152 - github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 153 - github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 206 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 207 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 154 208 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 209 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 210 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 155 211 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 156 212 github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 157 213 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= ··· 160 216 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 161 217 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 162 218 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 219 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 220 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 163 221 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 164 222 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 165 223 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 224 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 166 225 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 167 - github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 168 - github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 226 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 227 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 228 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 229 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 230 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 169 231 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 170 232 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 171 233 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 172 - github.com/whyrusleeping/cbor-gen v0.1.3-0.20240904181319-8dc02b38228c h1:UsxJNcLPfyLyVaA4iusIrsLAqJn/xh36Qgb8emqtXzk= 173 - github.com/whyrusleeping/cbor-gen v0.1.3-0.20240904181319-8dc02b38228c/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 234 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 235 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 236 + github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 237 + github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 174 238 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 175 239 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 176 240 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 241 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 177 242 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 178 243 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 179 244 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 180 245 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 246 + go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 247 + go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 181 248 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 182 249 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 183 - go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 184 - go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 185 - go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 186 - go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 187 - go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 188 - go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 250 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 251 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 252 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 253 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 254 + go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 255 + go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 256 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 257 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 258 + go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 259 + go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 260 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 261 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 262 + go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 263 + go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 189 264 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 190 265 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 191 266 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 206 281 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 207 282 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 208 283 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 209 - golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 210 - golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 284 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 285 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 286 + golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 287 + golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 288 + golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 289 + golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 211 290 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 212 291 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 213 292 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 214 293 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 215 294 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 295 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 296 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 216 297 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 217 298 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 218 299 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 219 300 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 220 301 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 302 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 221 303 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 222 - golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 223 - golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 304 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 305 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 306 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 307 + golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 308 + golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 224 309 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 310 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 311 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 312 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 228 315 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 316 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 317 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 318 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 319 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 320 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 321 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 234 322 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 323 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 324 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 325 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 326 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 327 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 237 - golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 238 - golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 328 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 329 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 330 + golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 331 + golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 332 + golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 333 + golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 239 334 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 335 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 336 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 337 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 338 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 240 339 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 241 340 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 242 - golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 243 - golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 341 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 342 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 343 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 344 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 345 + golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 346 + golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 244 347 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 245 348 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 246 349 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= ··· 251 354 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 252 355 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 253 356 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 357 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 358 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 254 359 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 360 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 256 361 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 257 362 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 258 363 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 259 364 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 365 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 366 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 260 367 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 261 368 google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 262 369 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= ··· 273 380 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 274 381 lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 275 382 lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 383 + lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= 384 + lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= 276 385 modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= 277 386 modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= 278 387 modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+6 -1
main.go
··· 83 83 go consumeLoop(ctx, store) 84 84 } 85 85 86 - server := NewServer(443, feeder, feedHost, feedDidBase, store) 86 + server, err := NewServer(443, feeder, feedHost, feedDidBase, store) 87 + if err != nil { 88 + slog.Error("create new server", "error", err) 89 + _ = bugsnag.Notify(err) 90 + return 91 + } 87 92 go func() { 88 93 <-signals 89 94 cancel()
+323
oauth_handlers.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "log/slog" 9 + "net/http" 10 + "net/url" 11 + "strings" 12 + 13 + "github.com/gorilla/sessions" 14 + oauth "github.com/haileyok/atproto-oauth-golang" 15 + oauthhelpers "github.com/haileyok/atproto-oauth-golang/helpers" 16 + "github.com/lestrrat-go/jwx/v2/jwk" 17 + "github.com/willdot/bskyfeedgen/frontend" 18 + "github.com/willdot/bskyfeedgen/store" 19 + ) 20 + 21 + const ( 22 + scope = "atproto transition:generic" 23 + ) 24 + 25 + func (s *Server) serverJwks(w http.ResponseWriter, r *http.Request) { 26 + w.Header().Set("Content-Type", "application/json") 27 + _, _ = w.Write(s.jwks.public) 28 + } 29 + 30 + func (s *Server) serveClientMetadata(w http.ResponseWriter, r *http.Request) { 31 + metadata := map[string]any{ 32 + "client_id": fmt.Sprintf("https://%s/client-metadata.json", s.feedHost), 33 + "client_name": "BS Feeder", 34 + "client_uri": fmt.Sprintf("https://%s", s.feedHost), 35 + "redirect_uris": []string{fmt.Sprintf("https://%s/oauth-callback", s.feedHost)}, 36 + "grant_types": []string{"authorization_code", "refresh_token"}, 37 + "response_types": []string{"code"}, 38 + "application_type": "web", 39 + "dpop_bound_access_tokens": true, 40 + "jwks_uri": fmt.Sprintf("https://%s/jwks.json", s.feedHost), 41 + "scope": "atproto transition:generic", 42 + "token_endpoint_auth_method": "private_key_jwt", 43 + "token_endpoint_auth_signing_alg": "ES256", 44 + } 45 + 46 + b, err := json.Marshal(metadata) 47 + if err != nil { 48 + slog.Error("failed to marshal client metadata", "error", err) 49 + http.Error(w, "marshal response", http.StatusInternalServerError) 50 + return 51 + } 52 + w.Header().Set("Content-Type", "application/json") 53 + _, _ = w.Write(b) 54 + } 55 + 56 + func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { 57 + b, err := io.ReadAll(r.Body) 58 + if err != nil { 59 + slog.Error("failed to read body", "error", err) 60 + _ = frontend.Login("", "bad request").Render(r.Context(), w) 61 + return 62 + } 63 + 64 + var loginReq loginRequest 65 + err = json.Unmarshal(b, &loginReq) 66 + if err != nil { 67 + slog.Error("failed to unmarshal body", "error", err) 68 + _ = frontend.Login("", "bad request").Render(r.Context(), w) 69 + return 70 + } 71 + 72 + usersDID, err := resolveHandle(loginReq.Handle) 73 + if err != nil { 74 + slog.Error("resolve users handle", "error", err) 75 + _ = frontend.Login("", "bad request").Render(r.Context(), w) 76 + return 77 + } 78 + 79 + dpopPrivateKey, err := oauthhelpers.GenerateKey(nil) 80 + if err != nil { 81 + slog.Error("generate key", "error", err) 82 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 83 + return 84 + } 85 + 86 + parResp, meta, err := s.parseLoginRequest(r.Context(), usersDID, loginReq.Handle, dpopPrivateKey) 87 + if err != nil { 88 + slog.Error("handle login request", "error", err) 89 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 90 + return 91 + } 92 + 93 + dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey) 94 + if err != nil { 95 + slog.Error("marshal key", "error", err) 96 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 97 + return 98 + } 99 + 100 + oauthRequst := store.OauthRequest{ 101 + AuthserverIss: meta.Issuer, 102 + State: parResp.State, 103 + Did: usersDID, 104 + PkceVerifier: parResp.PkceVerifier, 105 + DpopAuthserverNonce: parResp.DpopAuthserverNonce, 106 + DpopPrivateJwk: string(dpopPrivateKeyJson), 107 + } 108 + err = s.oauthRequestStore.CreateOauthRequest(oauthRequst) 109 + if err != nil { 110 + // TODO: catch already exists 111 + slog.Error("create oauth request in store", "error", err) 112 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 113 + return 114 + } 115 + 116 + u, _ := url.Parse(meta.AuthorizationEndpoint) 117 + u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(fmt.Sprintf("https://%s/client-metadata.json", s.feedHost)), parResp.RequestUri) 118 + 119 + // ignore error here as it only returns an error for decoding an existing session but it will always return a session anyway which 120 + // is what we want 121 + session, _ := s.sessionStore.Get(r, "oauth-session") 122 + session.Values = map[interface{}]interface{}{} 123 + 124 + session.Options = &sessions.Options{ 125 + Path: "/", 126 + MaxAge: 300, // save for five minutes 127 + HttpOnly: true, 128 + } 129 + 130 + session.Values["oauth_state"] = parResp.State 131 + session.Values["oauth_did"] = usersDID 132 + 133 + err = session.Save(r, w) 134 + if err != nil { 135 + slog.Error("save session", "error", err) 136 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 137 + return 138 + } 139 + 140 + w.Header().Add("HX-Redirect", u.String()) 141 + http.Redirect(w, r, u.String(), http.StatusOK) 142 + } 143 + 144 + func (s *Server) parseLoginRequest(ctx context.Context, did, handle string, dpopPrivateKey jwk.Key) (*oauth.SendParAuthResponse, *oauth.OauthAuthorizationMetadata, error) { 145 + service, err := resolveService(ctx, did) 146 + if err != nil { 147 + return nil, nil, err 148 + } 149 + 150 + authserver, err := s.oauthClient.ResolvePdsAuthServer(ctx, service) 151 + if err != nil { 152 + return nil, nil, err 153 + } 154 + 155 + meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver) 156 + if err != nil { 157 + return nil, nil, err 158 + } 159 + 160 + resp, err := s.oauthClient.SendParAuthRequest(ctx, authserver, meta, handle, scope, dpopPrivateKey) 161 + if err != nil { 162 + return nil, nil, err 163 + } 164 + return resp, meta, nil 165 + } 166 + 167 + func (s *Server) handleOauthCallback(w http.ResponseWriter, r *http.Request) { 168 + resState := r.FormValue("state") 169 + resIss := r.FormValue("iss") 170 + resCode := r.FormValue("code") 171 + 172 + session, err := s.sessionStore.Get(r, "oauth-session") 173 + if err != nil { 174 + slog.Error("getting session", "error", err) 175 + _ = frontend.Login("", "internal server error").Render(r.Context(), w) 176 + return 177 + } 178 + 179 + sessionState := session.Values["oauth_state"] 180 + 181 + if resState == "" || resIss == "" || resCode == "" { 182 + slog.Error("request missing needed parameters") 183 + _ = frontend.Login("", "internal server error").Render(r.Context(), w) 184 + return 185 + } 186 + 187 + if resState != sessionState { 188 + slog.Error("session state does not match response state") 189 + _ = frontend.Login("", "internal server error").Render(r.Context(), w) 190 + return 191 + } 192 + 193 + oauthRequest, err := s.oauthRequestStore.GetOauthRequest(fmt.Sprintf("%s", sessionState)) 194 + if err != nil { 195 + slog.Error("get oauth request from store", "error", err) 196 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 197 + return 198 + } 199 + 200 + err = s.oauthRequestStore.DeleteOauthRequest(fmt.Sprintf("%s", sessionState)) 201 + if err != nil { 202 + slog.Error("delete oauth request from store", "error", err) 203 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 204 + return 205 + } 206 + 207 + jwk, err := oauthhelpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk)) 208 + if err != nil { 209 + slog.Error("parse JWK", "error", err) 210 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 211 + return 212 + } 213 + 214 + initialTokenResp, err := s.oauthClient.InitialTokenRequest(r.Context(), resCode, resIss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk) 215 + if err != nil { 216 + slog.Error("getting token from request", "error", err) 217 + _ = frontend.Login("", "internal server error").Render(r.Context(), w) 218 + return 219 + } 220 + 221 + if initialTokenResp.Scope != scope { 222 + slog.Error("did not receive correct scopes from token request") 223 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 224 + return 225 + } 226 + 227 + session.Options = &sessions.Options{ 228 + Path: "/", 229 + MaxAge: 86400 * 7, 230 + HttpOnly: true, 231 + } 232 + 233 + // make sure the session is empty 234 + session.Values = map[interface{}]interface{}{} 235 + session.Values["did"] = oauthRequest.Did 236 + 237 + err = session.Save(r, w) 238 + if err != nil { 239 + slog.Error("save session", "error", err) 240 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 241 + return 242 + } 243 + 244 + // TODO: work out how the hell HTMX redirects really work 245 + s.HandleGetBookmarks(w, r) 246 + } 247 + 248 + func (s *Server) HandleSignOut(w http.ResponseWriter, r *http.Request) { 249 + session, err := s.sessionStore.Get(r, "oauth-session") 250 + if err != nil { 251 + slog.Error("getting session", "error", err) 252 + _ = frontend.Login("", "internal server error").Render(r.Context(), w) 253 + return 254 + } 255 + session.Values = map[interface{}]interface{}{} 256 + session.Options = &sessions.Options{ 257 + Path: "/", 258 + MaxAge: -1, 259 + HttpOnly: true, 260 + } 261 + 262 + err = session.Save(r, w) 263 + if err != nil { 264 + slog.Error("save session", "error", err) 265 + _ = frontend.Login("", "internal server errror").Render(r.Context(), w) 266 + return 267 + } 268 + 269 + _ = frontend.Login("", "").Render(r.Context(), w) 270 + } 271 + 272 + func resolveService(ctx context.Context, did string) (string, error) { 273 + type Identity struct { 274 + Service []struct { 275 + ID string `json:"id"` 276 + Type string `json:"type"` 277 + ServiceEndpoint string `json:"serviceEndpoint"` 278 + } `json:"service"` 279 + } 280 + 281 + var ustr string 282 + if strings.HasPrefix(did, "did:plc:") { 283 + ustr = fmt.Sprintf("https://plc.directory/%s", did) 284 + } else if strings.HasPrefix(did, "did:web:") { 285 + ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 286 + } else { 287 + return "", fmt.Errorf("did was not a supported did type") 288 + } 289 + 290 + req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 291 + if err != nil { 292 + return "", err 293 + } 294 + 295 + resp, err := http.DefaultClient.Do(req) 296 + if err != nil { 297 + return "", err 298 + } 299 + defer resp.Body.Close() 300 + 301 + if resp.StatusCode != 200 { 302 + io.Copy(io.Discard, resp.Body) 303 + return "", fmt.Errorf("could not find identity in plc registry") 304 + } 305 + 306 + var identity Identity 307 + if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil { 308 + return "", err 309 + } 310 + 311 + var service string 312 + for _, svc := range identity.Service { 313 + if svc.ID == "#atproto_pds" { 314 + service = svc.ServiceEndpoint 315 + } 316 + } 317 + 318 + if service == "" { 319 + return "", fmt.Errorf("could not find atproto_pds service in identity services") 320 + } 321 + 322 + return service, nil 323 + }
-49
public/styles.css
··· 611 611 height: 100vh; 612 612 } 613 613 614 - .h-3 { 615 - height: 0.75rem; 616 - } 617 - 618 614 .w-3\/12 { 619 615 width: 25%; 620 616 } ··· 627 623 width: 100%; 628 624 } 629 625 630 - .w-1\/3 { 631 - width: 33.333333%; 632 - } 633 - 634 626 .max-w-md { 635 627 max-width: 28rem; 636 628 } ··· 647 639 -webkit-appearance: none; 648 640 -moz-appearance: none; 649 641 appearance: none; 650 - } 651 - 652 - .flex-col { 653 - flex-direction: column; 654 642 } 655 643 656 644 .items-center { ··· 690 678 overflow: hidden; 691 679 } 692 680 693 - .overflow-x-auto { 694 - overflow-x: auto; 695 - } 696 - 697 681 .whitespace-nowrap { 698 682 white-space: nowrap; 699 683 } ··· 706 690 border-radius: 0.5rem; 707 691 } 708 692 709 - .rounded-b { 710 - border-bottom-right-radius: 0.25rem; 711 - border-bottom-left-radius: 0.25rem; 712 - } 713 - 714 693 .border { 715 694 border-width: 1px; 716 695 } 717 696 718 697 .border-2 { 719 698 border-width: 2px; 720 - } 721 - 722 - .border-t-0 { 723 - border-top-width: 0px; 724 699 } 725 700 726 701 .border-gray-200 { ··· 728 703 border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); 729 704 } 730 705 731 - .border-red-400 { 732 - --tw-border-opacity: 1; 733 - border-color: rgb(248 113 113 / var(--tw-border-opacity, 1)); 734 - } 735 - 736 706 .bg-blue-500 { 737 707 --tw-bg-opacity: 1; 738 708 background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); ··· 746 716 .bg-gray-50 { 747 717 --tw-bg-opacity: 1; 748 718 background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); 749 - } 750 - 751 - .bg-red-100 { 752 - --tw-bg-opacity: 1; 753 - background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); 754 719 } 755 720 756 721 .bg-white { ··· 797 762 padding-bottom: 0.5rem; 798 763 } 799 764 800 - .py-3 { 801 - padding-top: 0.75rem; 802 - padding-bottom: 0.75rem; 803 - } 804 - 805 765 .py-6 { 806 766 padding-top: 1.5rem; 807 767 padding-bottom: 1.5rem; ··· 817 777 818 778 .pt-6 { 819 779 padding-top: 1.5rem; 820 - } 821 - 822 - .pt-5 { 823 - padding-top: 1.25rem; 824 780 } 825 781 826 782 .text-center { ··· 890 846 .text-red-500 { 891 847 --tw-text-opacity: 1; 892 848 color: rgb(239 68 68 / var(--tw-text-opacity, 1)); 893 - } 894 - 895 - .text-red-700 { 896 - --tw-text-opacity: 1; 897 - color: rgb(185 28 28 / var(--tw-text-opacity, 1)); 898 849 } 899 850 900 851 .text-white {
+93 -18
server.go
··· 3 3 import ( 4 4 "context" 5 5 _ "embed" 6 + "encoding/base64" 7 + "encoding/json" 6 8 "fmt" 7 9 "log/slog" 8 10 "net/http" 11 + "os" 9 12 10 13 "github.com/bluesky-social/indigo/xrpc" 14 + "github.com/gorilla/sessions" 15 + oauth "github.com/haileyok/atproto-oauth-golang" 16 + oauthhelpers "github.com/haileyok/atproto-oauth-golang/helpers" 17 + "github.com/lestrrat-go/jwx/v2/jwk" 11 18 "github.com/willdot/bskyfeedgen/store" 12 19 ) 13 20 ··· 15 22 GetFeed(ctx context.Context, userDID, feed, cursor string, limit int) (FeedReponse, error) 16 23 } 17 24 25 + type Store interface { 26 + BookmarkStore 27 + OauthRequestStore 28 + } 29 + 18 30 type BookmarkStore interface { 19 31 CreateBookmark(postRKey, postURI, postATURI, authorDID, authorHandle, userDID, content string) error 20 32 GetBookmarksForUser(userDID string) ([]store.Bookmark, error) ··· 23 35 DeleteFeedPostsForBookmarkedPostURIandUserDID(subscribedPostURI, userDID string) error 24 36 } 25 37 38 + type OauthRequestStore interface { 39 + CreateOauthRequest(request store.OauthRequest) error 40 + GetOauthRequest(state string) (store.OauthRequest, error) 41 + DeleteOauthRequest(state string) error 42 + } 43 + 26 44 type Server struct { 27 - httpsrv *http.Server 28 - feeder Feeder 29 - feedHost string 30 - feedDidBase string 31 - bookmarkStore BookmarkStore 32 - xrpcClient *xrpc.Client 45 + httpsrv *http.Server 46 + feeder Feeder 47 + feedHost string 48 + feedDidBase string 49 + bookmarkStore BookmarkStore 50 + oauthRequestStore OauthRequestStore 51 + xrpcClient *xrpc.Client 52 + jwks *JWKS 53 + oauthClient *oauth.Client 54 + sessionStore *sessions.CookieStore 33 55 } 34 56 35 - func NewServer(port int, feeder Feeder, feedHost, feedDidBase string, bookmarkStore BookmarkStore) *Server { 57 + type JWKS struct { 58 + public []byte 59 + private jwk.Key 60 + } 61 + 62 + func NewServer(port int, feeder Feeder, feedHost, feedDidBase string, store Store) (*Server, error) { 63 + jwks, err := getJWKS() 64 + if err != nil { 65 + return nil, fmt.Errorf("create public JWKS: %w", err) 66 + } 67 + 68 + oauthClient, err := createOauthClient(jwks, fmt.Sprintf("https://%s", feedHost)) 69 + if err != nil { 70 + return nil, fmt.Errorf("create oauth client: %w", err) 71 + } 72 + 73 + sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 74 + 36 75 srv := &Server{ 37 - feeder: feeder, 38 - feedHost: feedHost, 39 - feedDidBase: feedDidBase, 40 - bookmarkStore: bookmarkStore, 76 + feeder: feeder, 77 + feedHost: feedHost, 78 + feedDidBase: feedDidBase, 79 + bookmarkStore: store, 80 + oauthRequestStore: store, 81 + jwks: jwks, 82 + oauthClient: oauthClient, 83 + sessionStore: sessionStore, 41 84 } 42 85 43 86 mux := http.NewServeMux() ··· 45 88 mux.HandleFunc("/xrpc/app.bsky.feed.getFeedSkeleton", srv.HandleGetFeedSkeleton) 46 89 mux.HandleFunc("/xrpc/app.bsky.feed.describeFeedGenerator", srv.HandleDescribeFeedGenerator) 47 90 mux.HandleFunc("/.well-known/did.json", srv.HandleWellKnown) 91 + mux.HandleFunc("/client-metadata.json", srv.serveClientMetadata) 92 + mux.HandleFunc("/jwks.json", srv.serverJwks) 93 + mux.HandleFunc("/oauth-callback", srv.handleOauthCallback) 48 94 49 95 mux.HandleFunc("/", srv.authMiddleware(srv.HandleGetBookmarks)) 50 96 mux.HandleFunc("/login", srv.HandleLogin) ··· 65 111 Host: "https://public.api.bsky.app", 66 112 } 67 113 68 - return srv 114 + return srv, nil 69 115 } 70 116 71 117 func (s *Server) Run() { ··· 87 133 _, _ = w.Write(cssFile) 88 134 } 89 135 90 - func getUsersDidFromRequestCookie(r *http.Request) (string, error) { 91 - didCookie, err := r.Cookie(didCookieName) 136 + func getJWKS() (*JWKS, error) { 137 + jwksB64 := os.Getenv("PRIVATEJWKS") 138 + if jwksB64 == "" { 139 + return nil, fmt.Errorf("PRIVATEJWKS env not set") 140 + } 141 + 142 + jwksB, err := base64.StdEncoding.DecodeString(jwksB64) 143 + if err != nil { 144 + return nil, fmt.Errorf("decode jwks env: %w", err) 145 + } 146 + 147 + k, err := oauthhelpers.ParseJWKFromBytes([]byte(jwksB)) 148 + if err != nil { 149 + return nil, fmt.Errorf("parse JWK from bytes: %w", err) 150 + } 151 + 152 + pubkey, err := k.PublicKey() 92 153 if err != nil { 93 - return "", err 154 + return nil, fmt.Errorf("get public key from JWKS: %w", err) 94 155 } 95 - if didCookie == nil { 96 - return "", fmt.Errorf("missing did cookie") 156 + 157 + resp := oauthhelpers.CreateJwksResponseObject(pubkey) 158 + b, err := json.Marshal(resp) 159 + if err != nil { 160 + return nil, fmt.Errorf("marshal public JWKS: %w", err) 97 161 } 98 162 99 - return didCookie.Value, nil 163 + return &JWKS{ 164 + public: b, 165 + private: k, 166 + }, nil 167 + } 168 + 169 + func createOauthClient(jwks *JWKS, serverBase string) (*oauth.Client, error) { 170 + return oauth.NewClient(oauth.ClientArgs{ 171 + ClientJwk: jwks.private, 172 + ClientId: fmt.Sprintf("%s/client-metadata.json", serverBase), 173 + RedirectUri: fmt.Sprintf("%s/oauth-callback", serverBase), 174 + }) 100 175 }
+5
store/database.go
··· 42 42 return nil, fmt.Errorf("creating bookmarks table: %w", err) 43 43 } 44 44 45 + err = createOauthRequestsTable(db) 46 + if err != nil { 47 + return nil, fmt.Errorf("creating oauth requests table: %w", err) 48 + } 49 + 45 50 return &Store{db: db}, nil 46 51 } 47 52
+87
store/oauth.go
··· 1 + package store 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + "fmt" 7 + "log/slog" 8 + ) 9 + 10 + var ErrOauthRequestAlreadyExists = errors.New("oauth request already exists") 11 + 12 + func createOauthRequestsTable(db *sql.DB) error { 13 + createOauthRequestsTableSQL := `CREATE TABLE IF NOT EXISTS oauthrequests ( 14 + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 15 + "authserverIss" TEXT, 16 + "state" TEXT, 17 + "did" TEXT, 18 + "pkceVerifier" TEXT, 19 + "dpopAuthserverNonce" TEXT, 20 + "dpopPrivateJwk" TEXT, 21 + UNIQUE(did,state) 22 + );` 23 + 24 + slog.Info("Create oauthrequests table...") 25 + statement, err := db.Prepare(createOauthRequestsTableSQL) 26 + if err != nil { 27 + return fmt.Errorf("prepare DB statement to create oauthrequests table: %w", err) 28 + } 29 + _, err = statement.Exec() 30 + if err != nil { 31 + return fmt.Errorf("exec sql statement to create oauthrequests table: %w", err) 32 + } 33 + slog.Info("oauthrequests table created") 34 + 35 + return nil 36 + } 37 + 38 + type OauthRequest struct { 39 + ID uint 40 + AuthserverIss string 41 + State string 42 + Did string 43 + PkceVerifier string 44 + DpopAuthserverNonce string 45 + DpopPrivateJwk string 46 + } 47 + 48 + func (s *Store) CreateOauthRequest(request OauthRequest) error { 49 + sql := `INSERT INTO oauthrequests (authserverIss, state, did, pkceVerifier, dpopAuthServerNonce, dpopPrivateJwk) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(did,state) DO NOTHING;` 50 + res, err := s.db.Exec(sql, request.AuthserverIss, request.State, request.Did, request.PkceVerifier, request.DpopAuthserverNonce, request.DpopPrivateJwk) 51 + if err != nil { 52 + return fmt.Errorf("exec insert oauth request: %w", err) 53 + } 54 + 55 + if x, _ := res.RowsAffected(); x == 0 { 56 + return ErrOauthRequestAlreadyExists 57 + } 58 + return nil 59 + } 60 + 61 + func (s *Store) GetOauthRequest(state string) (OauthRequest, error) { 62 + var oauthRequest OauthRequest 63 + sql := "SELECT authserverIss, state, did, pkceVerifier, dpopAuthServerNonce, dpopPrivateJwk FROM oauthrequests WHERE state = ?;" 64 + rows, err := s.db.Query(sql, state) 65 + if err != nil { 66 + return oauthRequest, fmt.Errorf("run query to get oauth request: %w", err) 67 + } 68 + defer rows.Close() 69 + 70 + for rows.Next() { 71 + if err := rows.Scan(&oauthRequest.AuthserverIss, &oauthRequest.State, &oauthRequest.Did, &oauthRequest.PkceVerifier, &oauthRequest.DpopAuthserverNonce, &oauthRequest.DpopPrivateJwk); err != nil { 72 + return oauthRequest, fmt.Errorf("scan row: %w", err) 73 + } 74 + 75 + return oauthRequest, nil 76 + } 77 + return oauthRequest, fmt.Errorf("not found") 78 + } 79 + 80 + func (s *Store) DeleteOauthRequest(state string) error { 81 + sql := "DELETE FROM oauthrequests WHERE state = ?;" 82 + _, err := s.db.Exec(sql, state) 83 + if err != nil { 84 + return fmt.Errorf("exec delete oauth request: %w", err) 85 + } 86 + return nil 87 + }