appview,knotserver: immutable nix flakeref link header #741

open
opened by boltless.me targeting master from push-ptrrwwvnkmxq
Changed files
+270 -250
appview
knotserver
lexicons
nix
-49
appview/repo/archive.go
··· 1 - package repo 2 - 3 - import ( 4 - "fmt" 5 - "net/http" 6 - "net/url" 7 - "strings" 8 - 9 - "tangled.org/core/api/tangled" 10 - xrpcclient "tangled.org/core/appview/xrpcclient" 11 - 12 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 - "github.com/go-chi/chi/v5" 14 - "github.com/go-git/go-git/v5/plumbing" 15 - ) 16 - 17 - func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 18 - l := rp.logger.With("handler", "DownloadArchive") 19 - ref := chi.URLParam(r, "ref") 20 - ref, _ = url.PathUnescape(ref) 21 - f, err := rp.repoResolver.Resolve(r) 22 - if err != nil { 23 - l.Error("failed to get repo and knot", "err", err) 24 - return 25 - } 26 - scheme := "http" 27 - if !rp.config.Core.Dev { 28 - scheme = "https" 29 - } 30 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 31 - xrpcc := &indigoxrpc.Client{ 32 - Host: host, 33 - } 34 - didSlashRepo := f.DidSlashRepo() 35 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo) 36 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 37 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 38 - rp.pages.Error503(w) 39 - return 40 - } 41 - // Set headers for file download, just pass along whatever the knot specifies 42 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 43 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 44 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 45 - w.Header().Set("Content-Type", "application/gzip") 46 - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 47 - // Write the archive data directly 48 - w.Write(archiveBytes) 49 - }
-4
appview/repo/router.go
··· 40 40 r.Get("/blob/{ref}/*", rp.Blob) 41 41 r.Get("/raw/{ref}/*", rp.RepoBlobRaw) 42 42 43 - // intentionally doesn't use /* as this isn't 44 - // a file path 45 - r.Get("/archive/{ref}", rp.DownloadArchive) 46 - 47 43 r.Route("/fork", func(r chi.Router) { 48 44 r.Use(middleware.AuthMiddleware(rp.oauth)) 49 45 r.Get("/", rp.ForkRepo)
-114
appview/state/git_http.go
··· 1 - package state 2 - 3 - import ( 4 - "fmt" 5 - "io" 6 - "maps" 7 - "net/http" 8 - 9 - "github.com/bluesky-social/indigo/atproto/identity" 10 - "github.com/go-chi/chi/v5" 11 - "tangled.org/core/appview/models" 12 - ) 13 - 14 - func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 - user := r.Context().Value("resolvedId").(identity.Identity) 16 - repo := r.Context().Value("repo").(*models.Repo) 17 - 18 - scheme := "https" 19 - if s.config.Core.Dev { 20 - scheme = "http" 21 - } 22 - 23 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 24 - s.proxyRequest(w, r, targetURL) 25 - 26 - } 27 - 28 - func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 - if !ok { 31 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 - return 33 - } 34 - repo := r.Context().Value("repo").(*models.Repo) 35 - 36 - scheme := "https" 37 - if s.config.Core.Dev { 38 - scheme = "http" 39 - } 40 - 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 - s.proxyRequest(w, r, targetURL) 43 - } 44 - 45 - func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 46 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 - if !ok { 48 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 - return 50 - } 51 - repo := r.Context().Value("repo").(*models.Repo) 52 - 53 - scheme := "https" 54 - if s.config.Core.Dev { 55 - scheme = "http" 56 - } 57 - 58 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 - s.proxyRequest(w, r, targetURL) 60 - } 61 - 62 - func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 63 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 64 - if !ok { 65 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 66 - return 67 - } 68 - repo := r.Context().Value("repo").(*models.Repo) 69 - 70 - scheme := "https" 71 - if s.config.Core.Dev { 72 - scheme = "http" 73 - } 74 - 75 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 76 - s.proxyRequest(w, r, targetURL) 77 - } 78 - 79 - func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 80 - client := &http.Client{} 81 - 82 - // Create new request 83 - proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 84 - if err != nil { 85 - http.Error(w, err.Error(), http.StatusInternalServerError) 86 - return 87 - } 88 - 89 - // Copy original headers 90 - proxyReq.Header = r.Header 91 - 92 - repoOwnerHandle := chi.URLParam(r, "user") 93 - proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 94 - 95 - // Execute request 96 - resp, err := client.Do(proxyReq) 97 - if err != nil { 98 - http.Error(w, err.Error(), http.StatusInternalServerError) 99 - return 100 - } 101 - defer resp.Body.Close() 102 - 103 - // Copy response headers 104 - maps.Copy(w.Header(), resp.Header) 105 - 106 - // Set response status code 107 - w.WriteHeader(resp.StatusCode) 108 - 109 - // Copy response body 110 - if _, err := io.Copy(w, resp.Body); err != nil { 111 - http.Error(w, err.Error(), http.StatusInternalServerError) 112 - return 113 - } 114 - }
+185
appview/state/proxy_knot.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "maps" 7 + "net/http" 8 + "strings" 9 + 10 + "github.com/bluesky-social/indigo/atproto/identity" 11 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 + "github.com/go-chi/chi/v5" 13 + "github.com/go-git/go-git/v5/plumbing" 14 + "github.com/hashicorp/go-version" 15 + "tangled.org/core/api/tangled" 16 + "tangled.org/core/appview/models" 17 + xrpcclient "tangled.org/core/appview/xrpcclient" 18 + ) 19 + 20 + func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 21 + user := r.Context().Value("resolvedId").(identity.Identity) 22 + repo := r.Context().Value("repo").(*models.Repo) 23 + 24 + scheme := "https" 25 + if s.config.Core.Dev { 26 + scheme = "http" 27 + } 28 + 29 + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 30 + s.proxyRequest(w, r, targetURL) 31 + 32 + } 33 + 34 + func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 35 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 36 + if !ok { 37 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 38 + return 39 + } 40 + repo := r.Context().Value("repo").(*models.Repo) 41 + 42 + scheme := "https" 43 + if s.config.Core.Dev { 44 + scheme = "http" 45 + } 46 + 47 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 48 + s.proxyRequest(w, r, targetURL) 49 + } 50 + 51 + func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 52 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 53 + if !ok { 54 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 55 + return 56 + } 57 + repo := r.Context().Value("repo").(*models.Repo) 58 + 59 + scheme := "https" 60 + if s.config.Core.Dev { 61 + scheme = "http" 62 + } 63 + 64 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 65 + s.proxyRequest(w, r, targetURL) 66 + } 67 + 68 + func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 69 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 70 + if !ok { 71 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 72 + return 73 + } 74 + repo := r.Context().Value("repo").(*models.Repo) 75 + 76 + scheme := "https" 77 + if s.config.Core.Dev { 78 + scheme = "http" 79 + } 80 + 81 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 82 + s.proxyRequest(w, r, targetURL) 83 + } 84 + 85 + var knotVersionDownloadArchiveConstraint = version.MustConstraints(version.NewConstraint(">= 1.12.0-alpha")) 86 + 87 + func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) { 88 + l := s.logger.With("handler", "DownloadArchive") 89 + ref := chi.URLParam(r, "ref") 90 + 91 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 92 + if !ok { 93 + l.Error("failed to resolve user") 94 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 95 + return 96 + } 97 + repo := r.Context().Value("repo").(*models.Repo) 98 + 99 + scheme := "https" 100 + if s.config.Core.Dev { 101 + scheme = "http" 102 + } 103 + 104 + host := fmt.Sprintf("%s://%s", scheme, repo.Knot) 105 + xrpcc := &indigoxrpc.Client{ 106 + Host: host, 107 + } 108 + l = l.With("knot", repo.Knot) 109 + 110 + isCompatible := func() bool { 111 + out, err := tangled.KnotVersion(r.Context(), xrpcc) 112 + if err != nil { 113 + l.Warn("failed to get knot version", "err", err) 114 + return false 115 + } 116 + 117 + v, err := version.NewVersion(out.Version) 118 + if err != nil { 119 + l.Warn("failed to parse knot version", "version", out.Version, "err", err) 120 + return false 121 + } 122 + 123 + if !knotVersionDownloadArchiveConstraint.Check(v) { 124 + l.Warn("knot version incompatible.", "version", v) 125 + return false 126 + } 127 + return true 128 + }() 129 + l.Debug("knot compatibility check", "isCompatible", isCompatible) 130 + if isCompatible { 131 + targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref) 132 + s.proxyRequest(w, r, targetURL) 133 + } else { 134 + l.Debug("requesting xrpc/sh.tangled.repo.archive") 135 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo.DidSlashRepo()) 136 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 137 + l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 138 + s.pages.Error503(w) 139 + return 140 + } 141 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 142 + filename := fmt.Sprintf("%s-%s.tar.gz", repo.Name, safeRefFilename) 143 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 144 + w.Header().Set("Content-Type", "application/gzip") 145 + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 146 + w.Write(archiveBytes) 147 + } 148 + } 149 + 150 + func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 151 + client := &http.Client{} 152 + 153 + // Create new request 154 + proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 155 + if err != nil { 156 + http.Error(w, err.Error(), http.StatusInternalServerError) 157 + return 158 + } 159 + 160 + // Copy original headers 161 + proxyReq.Header = r.Header 162 + 163 + repoOwnerHandle := chi.URLParam(r, "user") 164 + proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 165 + 166 + // Execute request 167 + resp, err := client.Do(proxyReq) 168 + if err != nil { 169 + http.Error(w, err.Error(), http.StatusInternalServerError) 170 + return 171 + } 172 + defer resp.Body.Close() 173 + 174 + // Copy response headers 175 + maps.Copy(w.Header(), resp.Header) 176 + 177 + // Set response status code 178 + w.WriteHeader(resp.StatusCode) 179 + 180 + // Copy response body 181 + if _, err := io.Copy(w, resp.Body); err != nil { 182 + http.Error(w, err.Error(), http.StatusInternalServerError) 183 + return 184 + } 185 + }
+3 -1
appview/state/router.go
··· 104 104 r.Post("/git-upload-archive", s.UploadArchive) 105 105 r.Post("/git-upload-pack", s.UploadPack) 106 106 r.Post("/git-receive-pack", s.ReceivePack) 107 - 107 + // intentionally doesn't use /* as this isn't 108 + // a file path 109 + r.Get("/archive/{ref}", s.DownloadArchive) 108 110 }) 109 111 }) 110 112
+1
go.mod
··· 131 131 github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect 132 132 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 133 133 github.com/hashicorp/go-sockaddr v1.0.7 // indirect 134 + github.com/hashicorp/go-version v1.8.0 // indirect 134 135 github.com/hashicorp/golang-lru v1.0.2 // indirect 135 136 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 136 137 github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
+2
go.sum
··· 264 264 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 265 265 github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= 266 266 github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= 267 + github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= 268 + github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 267 269 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 268 270 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 269 271 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+69
knotserver/archive.go
··· 1 + package knotserver 2 + 3 + import ( 4 + "compress/gzip" 5 + "fmt" 6 + "net/http" 7 + "strings" 8 + 9 + securejoin "github.com/cyphar/filepath-securejoin" 10 + "github.com/go-chi/chi/v5" 11 + "github.com/go-git/go-git/v5/plumbing" 12 + "tangled.org/core/knotserver/git" 13 + ) 14 + 15 + func (h *Knot) Archive(w http.ResponseWriter, r *http.Request) { 16 + var ( 17 + did = chi.URLParam(r, "did") 18 + name = chi.URLParam(r, "name") 19 + ref = chi.URLParam(r, "ref") 20 + ) 21 + repo, err := securejoin.SecureJoin(did, name) 22 + if err != nil { 23 + gitError(w, "repository not found", http.StatusNotFound) 24 + h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 25 + return 26 + } 27 + 28 + repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repo) 29 + if err != nil { 30 + gitError(w, "repository not found", http.StatusNotFound) 31 + h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 32 + return 33 + } 34 + 35 + gr, err := git.Open(repoPath, ref) 36 + 37 + immutableLink := fmt.Sprintf( 38 + "https://%s/%s/%s/archive/%s", 39 + h.c.Server.Hostname, 40 + did, 41 + name, 42 + gr.Hash(), 43 + ) 44 + 45 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 46 + filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 47 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 48 + w.Header().Set("Content-Type", "application/gzip") 49 + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 50 + 51 + gw := gzip.NewWriter(w) 52 + defer gw.Close() 53 + 54 + err = gr.WriteTar(gw, "") 55 + if err != nil { 56 + // once we start writing to the body we can't report error anymore 57 + // so we are only left with logging the error 58 + h.l.Error("writing tar file", "error", err) 59 + return 60 + } 61 + 62 + err = gw.Flush() 63 + if err != nil { 64 + // once we start writing to the body we can't report error anymore 65 + // so we are only left with logging the error 66 + h.l.Error("flushing", "error", err.Error()) 67 + return 68 + } 69 + }
+4
knotserver/git/git.go
··· 76 76 return &g, nil 77 77 } 78 78 79 + func (g *GitRepo) Hash() plumbing.Hash { 80 + return g.h 81 + } 82 + 79 83 // re-open a repository and update references 80 84 func (g *GitRepo) Refresh() error { 81 85 refreshed, err := PlainOpen(g.path)
+2
knotserver/router.go
··· 85 85 r.Post("/git-upload-archive", h.UploadArchive) 86 86 r.Post("/git-upload-pack", h.UploadPack) 87 87 r.Post("/git-receive-pack", h.ReceivePack) 88 + // convenience routes 89 + r.Get("/archive/{ref}", h.Archive) 88 90 }) 89 91 }) 90 92
-81
knotserver/xrpc/repo_archive.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "compress/gzip" 5 - "fmt" 6 - "net/http" 7 - "strings" 8 - 9 - "github.com/go-git/go-git/v5/plumbing" 10 - 11 - "tangled.org/core/knotserver/git" 12 - xrpcerr "tangled.org/core/xrpc/errors" 13 - ) 14 - 15 - func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) { 16 - repo := r.URL.Query().Get("repo") 17 - repoPath, err := x.parseRepoParam(repo) 18 - if err != nil { 19 - writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 - return 21 - } 22 - 23 - ref := r.URL.Query().Get("ref") 24 - // ref can be empty (git.Open handles this) 25 - 26 - format := r.URL.Query().Get("format") 27 - if format == "" { 28 - format = "tar.gz" // default 29 - } 30 - 31 - prefix := r.URL.Query().Get("prefix") 32 - 33 - if format != "tar.gz" { 34 - writeError(w, xrpcerr.NewXrpcError( 35 - xrpcerr.WithTag("InvalidRequest"), 36 - xrpcerr.WithMessage("only tar.gz format is supported"), 37 - ), http.StatusBadRequest) 38 - return 39 - } 40 - 41 - gr, err := git.Open(repoPath, ref) 42 - if err != nil { 43 - writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 44 - return 45 - } 46 - 47 - repoParts := strings.Split(repo, "/") 48 - repoName := repoParts[len(repoParts)-1] 49 - 50 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 51 - 52 - var archivePrefix string 53 - if prefix != "" { 54 - archivePrefix = prefix 55 - } else { 56 - archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename) 57 - } 58 - 59 - filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 60 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 61 - w.Header().Set("Content-Type", "application/gzip") 62 - 63 - gw := gzip.NewWriter(w) 64 - defer gw.Close() 65 - 66 - err = gr.WriteTar(gw, archivePrefix) 67 - if err != nil { 68 - // once we start writing to the body we can't report error anymore 69 - // so we are only left with logging the error 70 - x.Logger.Error("writing tar file", "error", err.Error()) 71 - return 72 - } 73 - 74 - err = gw.Flush() 75 - if err != nil { 76 - // once we start writing to the body we can't report error anymore 77 - // so we are only left with logging the error 78 - x.Logger.Error("flushing", "error", err.Error()) 79 - return 80 - } 81 - }
-1
knotserver/xrpc/xrpc.go
··· 64 64 r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare) 65 65 r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch) 66 66 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch) 67 - r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive) 68 67 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages) 69 68 70 69 // knot query endpoints (no auth required)
+1
lexicons/repo/archive.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", 7 + "description": "deprecated. use `/{did}/{reponame}/archive/{ref} endpoint instead", 7 8 "parameters": { 8 9 "type": "params", 9 10 "required": ["repo", "ref"],
+3
nix/gomod2nix.toml
··· 304 304 [mod."github.com/hashicorp/go-sockaddr"] 305 305 version = "v1.0.7" 306 306 hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs=" 307 + [mod."github.com/hashicorp/go-version"] 308 + version = "v1.8.0" 309 + hash = "sha256-KXtqERmYrWdpqPCViWcHbe6jnuH7k16bvBIcuJuevj8=" 307 310 [mod."github.com/hashicorp/golang-lru"] 308 311 version = "v1.0.2" 309 312 hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="