repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
9d325fb
parent
59eee85
author
Antonio Mika
date
2025-03-13 13:37:55 -0400 EDT
Merge pull request #190 from picosh/am/ssh-server

Started work on a crypto/ssh based ssh server
229 files changed,  +5485, -2004
M go.mod
M go.sum
M .dockerignore
+1, -0
1@@ -9,3 +9,4 @@ build/*
2 data/*
3 !data/.gitkeep
4 Dockerfile
5+ssh_data
M Dockerfile
+3, -3
 1@@ -54,8 +54,8 @@ ARG APP=prose
 2 
 3 COPY --from=builder-web /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
 4 COPY --from=builder-web /go/bin/${APP}-web ./web
 5-COPY --from=builder-web /app/${APP}/html ./${APP}/html
 6-COPY --from=builder-web /app/${APP}/public ./${APP}/public
 7+COPY --from=builder-web /app/pkg/apps/${APP}/html ./${APP}/html
 8+COPY --from=builder-web /app/pkg/apps/${APP}/public ./${APP}/public
 9 
10 ENTRYPOINT ["/app/web"]
11 
12@@ -69,7 +69,7 @@ ARG APP=prose
13 COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
14 COPY --from=builder-ssh /go/bin/${APP}-ssh ./ssh
15 # some services require the html folder
16-COPY --from=builder-ssh /app/${APP}/html ./${APP}/html
17+COPY --from=builder-ssh /app/pkg/apps/${APP}/html ./${APP}/html
18 
19 
20 ENTRYPOINT ["/app/ssh"]
M cmd/auth/web/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/auth"
5+import "github.com/picosh/pico/pkg/apps/auth"
6 
7 func main() {
8 	auth.StartApiServer()
M cmd/feeds/fetch/main.go
+2, -2
 1@@ -4,8 +4,8 @@ import (
 2 	"fmt"
 3 
 4 	"github.com/mmcdole/gofeed"
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/feeds"
 7+	"github.com/picosh/pico/pkg/apps/feeds"
 8+	"github.com/picosh/pico/pkg/db/postgres"
 9 )
10 
11 func main() {
M cmd/feeds/ssh/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/feeds"
5+import "github.com/picosh/pico/pkg/apps/feeds"
6 
7 func main() {
8 	feeds.StartSshServer()
M cmd/feeds/web/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/feeds"
5+import "github.com/picosh/pico/pkg/apps/feeds"
6 
7 func main() {
8 	feeds.StartApiServer()
M cmd/pastes/ssh/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/pastes"
5+import "github.com/picosh/pico/pkg/apps/pastes"
6 
7 func main() {
8 	pastes.StartSshServer()
M cmd/pastes/web/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/pastes"
5+import "github.com/picosh/pico/pkg/apps/pastes"
6 
7 func main() {
8 	pastes.StartApiServer()
M cmd/pgs/ssh/main.go
+4, -4
 1@@ -1,10 +1,10 @@
 2 package main
 3 
 4 import (
 5-	"github.com/picosh/pico/pgs"
 6-	pgsdb "github.com/picosh/pico/pgs/db"
 7-	"github.com/picosh/pico/shared"
 8-	"github.com/picosh/pico/shared/storage"
 9+	"github.com/picosh/pico/pkg/apps/pgs"
10+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
11+	"github.com/picosh/pico/pkg/shared"
12+	"github.com/picosh/pico/pkg/shared/storage"
13 	"github.com/picosh/utils"
14 )
15 
M cmd/pgs/web/main.go
+4, -4
 1@@ -1,10 +1,10 @@
 2 package main
 3 
 4 import (
 5-	"github.com/picosh/pico/pgs"
 6-	pgsdb "github.com/picosh/pico/pgs/db"
 7-	"github.com/picosh/pico/shared"
 8-	"github.com/picosh/pico/shared/storage"
 9+	"github.com/picosh/pico/pkg/apps/pgs"
10+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
11+	"github.com/picosh/pico/pkg/shared"
12+	"github.com/picosh/pico/pkg/shared/storage"
13 	"github.com/picosh/utils"
14 )
15 
M cmd/pico/ssh/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/pico"
5+import "github.com/picosh/pico/pkg/apps/pico"
6 
7 func main() {
8 	pico.StartSshServer()
M cmd/pipe/ssh/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/pipe"
5+import "github.com/picosh/pico/pkg/apps/pipe"
6 
7 func main() {
8 	pipe.StartSshServer()
M cmd/pipe/web/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/pipe"
5+import "github.com/picosh/pico/pkg/apps/pipe"
6 
7 func main() {
8 	pipe.StartApiServer()
M cmd/prose/ssh/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/prose"
5+import "github.com/picosh/pico/pkg/apps/prose"
6 
7 func main() {
8 	prose.StartSshServer()
M cmd/prose/web/main.go
+1, -1
1@@ -1,6 +1,6 @@
2 package main
3 
4-import "github.com/picosh/pico/prose"
5+import "github.com/picosh/pico/pkg/apps/prose"
6 
7 func main() {
8 	prose.StartApiServer()
M cmd/scripts/analytics/analytics.go
+2, -2
 1@@ -4,8 +4,8 @@ import (
 2 	"log/slog"
 3 	"os"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7+	"github.com/picosh/pico/pkg/db"
 8+	"github.com/picosh/pico/pkg/db/postgres"
 9 	"github.com/picosh/utils"
10 )
11 
M cmd/scripts/clean-analytics/clean.go
+2, -2
 1@@ -5,8 +5,8 @@ import (
 2 	"log/slog"
 3 	"os"
 4 
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db/postgres"
 8+	"github.com/picosh/pico/pkg/shared"
 9 )
10 
11 func main() {
M cmd/scripts/clean-object-store/clean.go
+5, -5
 1@@ -5,11 +5,11 @@ import (
 2 	"os"
 3 	"strings"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/pgs"
 7-	pgsdb "github.com/picosh/pico/pgs/db"
 8-	"github.com/picosh/pico/shared"
 9-	"github.com/picosh/pico/shared/storage"
10+	"github.com/picosh/pico/pkg/apps/pgs"
11+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
12+	"github.com/picosh/pico/pkg/db"
13+	"github.com/picosh/pico/pkg/shared"
14+	"github.com/picosh/pico/pkg/shared/storage"
15 	"github.com/picosh/utils"
16 )
17 
M cmd/scripts/dates/dates.go
+3, -3
 1@@ -7,9 +7,9 @@ import (
 2 	"os"
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/db/postgres"
10+	"github.com/picosh/pico/pkg/shared"
11 )
12 
13 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
M cmd/scripts/file-size-sync/sync.go
+2, -2
 1@@ -5,8 +5,8 @@ import (
 2 	"log/slog"
 3 	"os"
 4 
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db/postgres"
 8+	"github.com/picosh/pico/pkg/shared"
 9 )
10 
11 func bail(err error) {
M cmd/scripts/migrate/migrate.go
+3, -3
 1@@ -7,9 +7,9 @@ import (
 2 	"log/slog"
 3 	"os"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/db/postgres"
10+	"github.com/picosh/pico/pkg/shared"
11 )
12 
13 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
M cmd/scripts/pico-plus/main.go
+1, -1
1@@ -4,7 +4,7 @@ import (
2 	"log/slog"
3 	"os"
4 
5-	"github.com/picosh/pico/db/postgres"
6+	"github.com/picosh/pico/pkg/db/postgres"
7 )
8 
9 func main() {
M cmd/scripts/prose-imgs-migrate/main.go
+7, -7
 1@@ -7,13 +7,13 @@ import (
 2 	"path/filepath"
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/prose"
 8-	"github.com/picosh/pico/shared"
 9-	"github.com/picosh/pico/shared/storage"
10-	sst "github.com/picosh/pobj/storage"
11-	sendUtils "github.com/picosh/send/utils"
12+	"github.com/picosh/pico/pkg/apps/prose"
13+	"github.com/picosh/pico/pkg/db"
14+	"github.com/picosh/pico/pkg/db/postgres"
15+	sst "github.com/picosh/pico/pkg/pobj/storage"
16+	sendUtils "github.com/picosh/pico/pkg/send/utils"
17+	"github.com/picosh/pico/pkg/shared"
18+	"github.com/picosh/pico/pkg/shared/storage"
19 )
20 
21 func bail(err error) {
M cmd/scripts/rm-old-buckets/rm-old-buckets.go
+3, -3
 1@@ -6,9 +6,9 @@ import (
 2 
 3 	"github.com/minio/minio-go/v7"
 4 	"github.com/minio/minio-go/v7/pkg/credentials"
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/prose"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/apps/prose"
 9+	"github.com/picosh/pico/pkg/db/postgres"
10+	"github.com/picosh/pico/pkg/shared"
11 )
12 
13 func bail(err error) {
M cmd/scripts/shasum/shasum.go
+2, -2
 1@@ -4,8 +4,8 @@ import (
 2 	"log/slog"
 3 	"os"
 4 
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db/postgres"
 8+	"github.com/picosh/pico/pkg/shared"
 9 	"github.com/picosh/utils"
10 )
11 
M cmd/scripts/tags/tags.go
+3, -3
 1@@ -5,9 +5,9 @@ import (
 2 	"log/slog"
 3 	"os"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/db/postgres"
10+	"github.com/picosh/pico/pkg/shared"
11 )
12 
13 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
D feeds/ssh.go
+0, -114
  1@@ -1,114 +0,0 @@
  2-package feeds
  3-
  4-import (
  5-	"context"
  6-	"fmt"
  7-	"os"
  8-	"os/signal"
  9-	"syscall"
 10-	"time"
 11-
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15-	"github.com/picosh/pico/db/postgres"
 16-	"github.com/picosh/pico/filehandlers"
 17-	"github.com/picosh/pico/shared"
 18-	wsh "github.com/picosh/pico/wish"
 19-	"github.com/picosh/send/auth"
 20-	"github.com/picosh/send/list"
 21-	"github.com/picosh/send/pipe"
 22-	wishrsync "github.com/picosh/send/protocols/rsync"
 23-	"github.com/picosh/send/protocols/scp"
 24-	"github.com/picosh/send/protocols/sftp"
 25-	"github.com/picosh/send/proxy"
 26-	"github.com/picosh/utils"
 27-)
 28-
 29-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 30-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 31-		return []wish.Middleware{
 32-			pipe.Middleware(handler, ".txt"),
 33-			list.Middleware(handler),
 34-			scp.Middleware(handler),
 35-			wishrsync.Middleware(handler),
 36-			auth.Middleware(handler),
 37-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 38-			WishMiddleware(handler.DBPool, handler.Cfg),
 39-			wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool),
 40-		}
 41-	}
 42-}
 43-
 44-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 45-	return func(server *ssh.Server) error {
 46-		err := sftp.SSHOption(handler)(server)
 47-		if err != nil {
 48-			return err
 49-		}
 50-
 51-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 52-
 53-		for name, subsystemHandler := range server.SubsystemHandlers {
 54-			newSubsystemHandlers[name] = func(s ssh.Session) {
 55-				wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool)(ssh.Handler(subsystemHandler))(s)
 56-			}
 57-		}
 58-
 59-		server.SubsystemHandlers = newSubsystemHandlers
 60-
 61-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 62-	}
 63-}
 64-
 65-func StartSshServer() {
 66-	host := utils.GetEnv("LISTS_HOST", "0.0.0.0")
 67-	port := utils.GetEnv("LISTS_SSH_PORT", "2222")
 68-	promPort := utils.GetEnv("LISTS_PROM_PORT", "9222")
 69-	cfg := NewConfigSite("feeds-ssh")
 70-	logger := cfg.Logger
 71-	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 72-	defer dbh.Close()
 73-
 74-	hooks := &FeedHooks{
 75-		Cfg: cfg,
 76-		Db:  dbh,
 77-	}
 78-
 79-	fileMap := map[string]filehandlers.ReadWriteHandler{
 80-		"fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 81-	}
 82-	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 83-
 84-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 85-	s, err := wish.NewServer(
 86-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 87-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 88-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
 89-		withProxy(
 90-			handler,
 91-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "feeds-ssh"),
 92-		),
 93-	)
 94-	if err != nil {
 95-		logger.Error(err.Error())
 96-		return
 97-	}
 98-
 99-	done := make(chan os.Signal, 1)
100-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
101-	logger.Info("Starting SSH server", "host", host, "port", port)
102-	go func() {
103-		if err = s.ListenAndServe(); err != nil {
104-			logger.Error(err.Error())
105-		}
106-	}()
107-
108-	<-done
109-	logger.Info("Stopping SSH server")
110-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
111-	defer func() { cancel() }()
112-	if err := s.Shutdown(ctx); err != nil {
113-		logger.Error(err.Error())
114-	}
115-}
M go.mod
+126, -158
  1@@ -4,23 +4,32 @@ go 1.24
  2 
  3 toolchain go1.24.0
  4 
  5+// replace github.com/picosh/tunkit => ../tunkit
  6+
  7 // replace github.com/picosh/send => ../send
  8-// replace git.sr.ht/~rockorager/vaxis => ../../../src/vaxis
  9+
 10+// replace github.com/picosh/go-rsync-receiver => ../go-rsync-receiver
 11+
 12+// replace github.com/picosh/pobj => ../pobj
 13+
 14+// replace github.com/picosh/pubsub => ../pubsub
 15+
 16+// replace github.com/picosh/utils => ../utils
 17+
 18+// replace git.sr.ht/~delthas/senpai => ../../senpai
 19+
 20+// replace git.sr.ht/~rockorager/vaxis => ../../vaxis
 21 
 22 require (
 23 	git.sr.ht/~delthas/senpai v0.3.1-0.20250311003540-18f699aaf9b0
 24 	git.sr.ht/~rockorager/vaxis v0.12.1-0.20250312161844-81636f76af83
 25-	github.com/alecthomas/chroma/v2 v2.14.0
 26+	github.com/alecthomas/chroma/v2 v2.15.0
 27 	github.com/antoniomika/syncmap v1.0.0
 28 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
 29-	github.com/charmbracelet/lipgloss v1.0.0
 30-	github.com/charmbracelet/promwish v0.7.0
 31-	github.com/charmbracelet/ssh v0.0.0-20250213143314-8712ec3ff3ef
 32-	github.com/charmbracelet/wish v1.4.6
 33 	github.com/containerd/console v1.0.4
 34 	github.com/darkweak/souin v1.7.5
 35 	github.com/darkweak/souin/plugins/souin/storages v1.7.5
 36-	github.com/darkweak/storages/core v0.0.11
 37+	github.com/darkweak/storages/core v0.0.13
 38 	github.com/gkampitakis/go-snaps v0.5.7
 39 	github.com/google/go-cmp v0.7.0
 40 	github.com/google/uuid v1.6.0
 41@@ -28,116 +37,87 @@ require (
 42 	github.com/gorilla/websocket v1.5.3
 43 	github.com/jmoiron/sqlx v1.4.0
 44 	github.com/lib/pq v1.10.9
 45+	github.com/matryer/is v1.4.1
 46 	github.com/microcosm-cc/bluemonday v1.0.27
 47-	github.com/minio/minio-go/v7 v7.0.87
 48+	github.com/minio/madmin-go/v3 v3.0.97
 49+	github.com/minio/minio-go/v7 v7.0.88
 50 	github.com/mmcdole/gofeed v1.3.0
 51-	github.com/muesli/termenv v0.16.0
 52 	github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577
 53-	github.com/picosh/pobj v0.0.0-20250304201248-a9c7179aa49b
 54+	github.com/picosh/go-rsync-receiver v0.0.0-20250304201040-fcc11dd22d79
 55 	github.com/picosh/pubsub v0.0.0-20241114191831-ec8f16c0eb88
 56-	github.com/picosh/send v0.0.0-20250304201154-e36cd3bbbb35
 57-	github.com/picosh/tunkit v0.0.0-20240905223921-532404cef9d9
 58 	github.com/picosh/utils v0.0.0-20241120033529-8ca070c09bf4
 59-	github.com/pkg/sftp v1.13.7
 60-	github.com/prometheus/client_golang v1.21.0-rc.0
 61+	github.com/pkg/sftp v1.13.8
 62+	github.com/prometheus/client_golang v1.21.1
 63 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
 64 	github.com/sendgrid/sendgrid-go v3.16.0+incompatible
 65 	github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
 66-	github.com/x-way/crawlerdetect v0.2.24
 67+	github.com/x-way/crawlerdetect v0.2.28
 68 	github.com/yuin/goldmark v1.7.8
 69 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 70 	github.com/yuin/goldmark-meta v1.1.0
 71-	go.abhg.dev/goldmark/anchor v0.1.1
 72+	go.abhg.dev/goldmark/anchor v0.2.0
 73 	go.abhg.dev/goldmark/hashtag v0.3.1
 74-	go.abhg.dev/goldmark/toc v0.10.0
 75+	go.abhg.dev/goldmark/toc v0.11.0
 76 	golang.org/x/crypto v0.36.0
 77 	google.golang.org/protobuf v1.36.5
 78 	gopkg.in/yaml.v2 v2.4.0
 79 )
 80 
 81 require (
 82+	cel.dev/expr v0.22.0 // indirect
 83 	codeberg.org/emersion/go-scfg v0.1.0 // indirect
 84-	dario.cat/mergo v1.0.0 // indirect
 85+	dario.cat/mergo v1.0.1 // indirect
 86 	filippo.io/edwards25519 v1.1.0 // indirect
 87 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 88 	github.com/Masterminds/goutils v1.1.1 // indirect
 89-	github.com/Masterminds/semver/v3 v3.2.0 // indirect
 90-	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
 91-	github.com/Microsoft/go-winio v0.6.1 // indirect
 92-	github.com/PuerkitoBio/goquery v1.10.0 // indirect
 93-	github.com/RoaringBitmap/roaring v1.2.3 // indirect
 94-	github.com/andybalholm/cascadia v1.3.2 // indirect
 95-	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
 96-	github.com/antlabs/stl v0.0.1 // indirect
 97-	github.com/antlabs/timer v0.0.11 // indirect
 98-	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 99+	github.com/Masterminds/semver/v3 v3.3.1 // indirect
100+	github.com/Masterminds/sprig/v3 v3.3.0 // indirect
101+	github.com/Microsoft/go-winio v0.6.2 // indirect
102+	github.com/PuerkitoBio/goquery v1.10.2 // indirect
103+	github.com/RoaringBitmap/roaring v1.9.4 // indirect
104+	github.com/andybalholm/cascadia v1.3.3 // indirect
105+	github.com/antlabs/stl v0.0.2 // indirect
106+	github.com/antlabs/timer v0.1.4 // indirect
107+	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
108 	github.com/armon/go-metrics v0.4.1 // indirect
109 	github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
110-	github.com/aws/aws-sdk-go-v2 v1.36.2 // indirect
111-	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
112 	github.com/aws/aws-sdk-go-v2/config v1.29.7 // indirect
113-	github.com/aws/aws-sdk-go-v2/credentials v1.17.60 // indirect
114-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 // indirect
115-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.63 // indirect
116-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect
117-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect
118-	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
119-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.33 // indirect
120-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
121-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.1 // indirect
122-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 // indirect
123-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.14 // indirect
124-	github.com/aws/aws-sdk-go-v2/service/s3 v1.77.1 // indirect
125-	github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 // indirect
126-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 // indirect
127-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 // indirect
128 	github.com/aws/smithy-go v1.22.3 // indirect
129-	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
130 	github.com/aymerick/douceur v0.2.0 // indirect
131 	github.com/beorn7/perks v1.0.1 // indirect
132-	github.com/bits-and-blooms/bitset v1.5.0 // indirect
133+	github.com/bits-and-blooms/bitset v1.22.0 // indirect
134 	github.com/buraksezer/consistent v0.10.0 // indirect
135-	github.com/buraksezer/olric v0.5.6 // indirect
136+	github.com/buraksezer/olric v0.5.7 // indirect
137 	github.com/bwmarrin/snowflake v0.3.0 // indirect
138-	github.com/caddyserver/caddy/v2 v2.8.4 // indirect
139-	github.com/caddyserver/certmagic v0.21.3 // indirect
140+	github.com/caddyserver/caddy/v2 v2.9.1 // indirect
141+	github.com/caddyserver/certmagic v0.22.0 // indirect
142 	github.com/caddyserver/zerossl v0.1.3 // indirect
143+	github.com/ccoveille/go-safecast v1.6.0 // indirect
144 	github.com/cespare/xxhash v1.1.0 // indirect
145 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
146-	github.com/charmbracelet/bubbletea v1.3.4 // indirect
147-	github.com/charmbracelet/keygen v0.5.1 // indirect
148-	github.com/charmbracelet/log v0.4.0 // indirect
149-	github.com/charmbracelet/x/ansi v0.8.0 // indirect
150-	github.com/charmbracelet/x/conpty v0.1.0 // indirect
151-	github.com/charmbracelet/x/errors v0.0.0-20250226164017-59292a315e58 // indirect
152-	github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect
153-	github.com/charmbracelet/x/input v0.3.1 // indirect
154-	github.com/charmbracelet/x/term v0.2.1 // indirect
155-	github.com/charmbracelet/x/termios v0.1.1 // indirect
156-	github.com/charmbracelet/x/windows v0.2.0 // indirect
157 	github.com/chzyer/readline v1.5.1 // indirect
158+	github.com/coreos/go-oidc/v3 v3.12.0 // indirect
159 	github.com/coreos/go-semver v0.3.1 // indirect
160 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
161-	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
162-	github.com/creack/pty v1.1.24 // indirect
163-	github.com/darkweak/go-esi v0.0.5 // indirect
164-	github.com/darkweak/storages/badger v0.0.8 // indirect
165-	github.com/darkweak/storages/etcd v0.0.8 // indirect
166-	github.com/darkweak/storages/nats v0.0.8 // indirect
167-	github.com/darkweak/storages/nuts v0.0.8 // indirect
168-	github.com/darkweak/storages/olric v0.0.8 // indirect
169-	github.com/darkweak/storages/otter v0.0.11 // indirect
170-	github.com/darkweak/storages/redis v0.0.8 // indirect
171+	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
172+	github.com/darkweak/go-esi v0.0.6 // indirect
173+	github.com/darkweak/storages/badger v0.0.13 // indirect
174+	github.com/darkweak/storages/etcd v0.0.13 // indirect
175+	github.com/darkweak/storages/nats v0.0.13 // indirect
176+	github.com/darkweak/storages/nuts v0.0.13 // indirect
177+	github.com/darkweak/storages/olric v0.0.13 // indirect
178+	github.com/darkweak/storages/otter v0.0.13 // indirect
179+	github.com/darkweak/storages/redis v0.0.13 // indirect
180 	github.com/delthas/go-libnp v0.0.0-20250105150050-96674b98150e // indirect
181 	github.com/delthas/go-localeinfo v0.0.0-20240813094314-e5413e186769 // indirect
182 	github.com/dgraph-io/badger v1.6.2 // indirect
183 	github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
184 	github.com/dgraph-io/badger/v3 v3.2103.5 // indirect
185-	github.com/dgraph-io/ristretto v0.1.1 // indirect
186-	github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
187+	github.com/dgraph-io/ristretto v0.2.0 // indirect
188+	github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
189 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
190 	github.com/disintegration/imaging v1.6.2 // indirect
191-	github.com/dlclark/regexp2 v1.11.4 // indirect
192+	github.com/dlclark/regexp2 v1.11.5 // indirect
193 	github.com/dolthub/maphash v0.1.0 // indirect
194 	github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect
195 	github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
196@@ -147,79 +127,69 @@ require (
197 	github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
198 	github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
199 	github.com/dustin/go-humanize v1.0.1 // indirect
200-	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
201-	github.com/forPelevin/gomoji v1.2.0 // indirect
202-	github.com/gammazero/deque v0.2.1 // indirect
203+	github.com/forPelevin/gomoji v1.3.0 // indirect
204+	github.com/francoispqt/gojay v1.2.13 // indirect
205+	github.com/gammazero/deque v1.0.0 // indirect
206 	github.com/gkampitakis/ciinfo v0.3.0 // indirect
207 	github.com/gkampitakis/go-diff v1.3.2 // indirect
208 	github.com/go-errors/errors v1.5.1 // indirect
209 	github.com/go-ini/ini v1.67.0 // indirect
210-	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
211-	github.com/go-kit/kit v0.13.0 // indirect
212-	github.com/go-kit/log v0.2.1 // indirect
213-	github.com/go-logfmt/logfmt v0.6.0 // indirect
214+	github.com/go-jose/go-jose/v3 v3.0.4 // indirect
215+	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
216 	github.com/go-ole/go-ole v1.3.0 // indirect
217 	github.com/go-redis/redis/v8 v8.11.5 // indirect
218-	github.com/go-sql-driver/mysql v1.8.1 // indirect
219-	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
220-	github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
221+	github.com/go-sql-driver/mysql v1.9.0 // indirect
222+	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
223+	github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
224 	github.com/goccy/go-json v0.10.5 // indirect
225 	github.com/godbus/dbus/v5 v5.1.0 // indirect
226-	github.com/gofrs/flock v0.8.1 // indirect
227+	github.com/gofrs/flock v0.12.1 // indirect
228 	github.com/gogo/protobuf v1.3.2 // indirect
229 	github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
230 	github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
231-	github.com/golang/glog v1.2.3 // indirect
232-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
233+	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
234 	github.com/golang/protobuf v1.5.4 // indirect
235-	github.com/golang/snappy v0.0.4 // indirect
236-	github.com/google/btree v1.1.2 // indirect
237-	github.com/google/cel-go v0.20.1 // indirect
238-	github.com/google/flatbuffers v23.1.21+incompatible // indirect
239-	github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
240+	github.com/golang/snappy v1.0.0 // indirect
241+	github.com/google/btree v1.1.3 // indirect
242+	github.com/google/cel-go v0.24.1 // indirect
243+	github.com/google/flatbuffers v25.2.10+incompatible // indirect
244+	github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
245 	github.com/gorilla/css v1.0.1 // indirect
246 	github.com/hashicorp/errwrap v1.1.0 // indirect
247 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
248-	github.com/hashicorp/go-msgpack v0.5.5 // indirect
249+	github.com/hashicorp/go-metrics v0.5.4 // indirect
250+	github.com/hashicorp/go-msgpack/v2 v2.1.3 // indirect
251 	github.com/hashicorp/go-multierror v1.1.1 // indirect
252-	github.com/hashicorp/go-sockaddr v1.0.2 // indirect
253-	github.com/hashicorp/golang-lru v0.6.0 // indirect
254+	github.com/hashicorp/go-sockaddr v1.0.7 // indirect
255+	github.com/hashicorp/golang-lru v1.0.2 // indirect
256 	github.com/hashicorp/logutils v1.0.0 // indirect
257-	github.com/hashicorp/memberlist v0.5.0 // indirect
258-	github.com/huandu/xstrings v1.3.3 // indirect
259-	github.com/imdario/mergo v0.3.16 // indirect
260+	github.com/hashicorp/memberlist v0.5.3 // indirect
261+	github.com/huandu/xstrings v1.5.0 // indirect
262 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
263-	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
264-	github.com/jackc/pgconn v1.14.3 // indirect
265-	github.com/jackc/pgio v1.0.0 // indirect
266 	github.com/jackc/pgpassfile v1.0.0 // indirect
267-	github.com/jackc/pgproto3/v2 v2.3.3 // indirect
268-	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
269-	github.com/jackc/pgtype v1.14.0 // indirect
270-	github.com/jackc/pgx/v4 v4.18.3 // indirect
271+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
272+	github.com/jackc/pgx/v5 v5.7.2 // indirect
273+	github.com/jackc/puddle/v2 v2.2.2 // indirect
274 	github.com/json-iterator/go v1.1.12 // indirect
275 	github.com/klauspost/compress v1.18.0 // indirect
276 	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
277 	github.com/kr/fs v0.1.0 // indirect
278 	github.com/kr/pretty v0.3.1 // indirect
279 	github.com/kr/text v0.2.0 // indirect
280-	github.com/libdns/libdns v0.2.2 // indirect
281-	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
282-	github.com/lufia/plan9stats v0.0.0-20250224150550-a661cff19cfb // indirect
283+	github.com/libdns/libdns v0.2.3 // indirect
284+	github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d // indirect
285 	github.com/manifoldco/promptui v0.9.0 // indirect
286 	github.com/maruel/natural v1.1.1 // indirect
287-	github.com/mattn/go-colorable v0.1.13 // indirect
288+	github.com/mattn/go-colorable v0.1.14 // indirect
289 	github.com/mattn/go-isatty v0.0.20 // indirect
290-	github.com/mattn/go-localereader v0.0.1 // indirect
291 	github.com/mattn/go-runewidth v0.0.16 // indirect
292 	github.com/mattn/go-sixel v0.0.5 // indirect
293 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
294-	github.com/maypok86/otter v1.2.1 // indirect
295+	github.com/maypok86/otter v1.2.4 // indirect
296 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
297-	github.com/mholt/acmez/v2 v2.0.1 // indirect
298+	github.com/mholt/acmez/v3 v3.1.0 // indirect
299 	github.com/miekg/dns v1.1.63 // indirect
300 	github.com/minio/crc64nvme v1.0.1 // indirect
301-	github.com/minio/madmin-go/v3 v3.0.94 // indirect
302 	github.com/minio/md5-simd v1.1.2 // indirect
303 	github.com/mitchellh/copystructure v1.2.0 // indirect
304 	github.com/mitchellh/go-ps v1.0.0 // indirect
305@@ -229,18 +199,15 @@ require (
306 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
307 	github.com/modern-go/reflect2 v1.0.2 // indirect
308 	github.com/mschoch/smat v0.2.0 // indirect
309-	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
310-	github.com/muesli/cancelreader v0.2.2 // indirect
311 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
312-	github.com/nats-io/nats.go v1.36.0 // indirect
313-	github.com/nats-io/nkeys v0.4.7 // indirect
314+	github.com/nats-io/nats.go v1.39.1 // indirect
315+	github.com/nats-io/nkeys v0.4.10 // indirect
316 	github.com/nats-io/nuid v1.0.1 // indirect
317 	github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e // indirect
318 	github.com/nutsdb/nutsdb v1.0.4 // indirect
319-	github.com/onsi/ginkgo/v2 v2.15.0 // indirect
320+	github.com/onsi/ginkgo/v2 v2.23.0 // indirect
321 	github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
322-	github.com/picosh/go-rsync-receiver v0.0.0-20250304201040-fcc11dd22d79 // indirect
323-	github.com/pierrec/lz4/v4 v4.1.21 // indirect
324+	github.com/pierrec/lz4/v4 v4.1.22 // indirect
325 	github.com/pkg/errors v0.9.1 // indirect
326 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
327 	github.com/pquerna/cachecontrol v0.2.0 // indirect
328@@ -249,9 +216,9 @@ require (
329 	github.com/prometheus/procfs v0.15.1 // indirect
330 	github.com/prometheus/prom2json v1.4.1 // indirect
331 	github.com/prometheus/prometheus v0.302.1 // indirect
332-	github.com/quic-go/qpack v0.4.0 // indirect
333-	github.com/quic-go/quic-go v0.44.0 // indirect
334-	github.com/redis/rueidis v1.0.39 // indirect
335+	github.com/quic-go/qpack v0.5.1 // indirect
336+	github.com/quic-go/quic-go v0.50.0 // indirect
337+	github.com/redis/rueidis v1.0.55 // indirect
338 	github.com/rivo/uniseg v0.4.7 // indirect
339 	github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a // indirect
340 	github.com/rs/xid v1.6.0 // indirect
341@@ -262,65 +229,66 @@ require (
342 	github.com/sendgrid/rest v2.6.9+incompatible // indirect
343 	github.com/shirou/gopsutil/v3 v3.24.5 // indirect
344 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
345-	github.com/shopspring/decimal v1.2.0 // indirect
346+	github.com/shopspring/decimal v1.4.0 // indirect
347 	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
348-	github.com/slackhq/nebula v1.6.1 // indirect
349-	github.com/smallstep/certificates v0.26.1 // indirect
350-	github.com/smallstep/nosql v0.6.1 // indirect
351-	github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
352-	github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
353+	github.com/slackhq/nebula v1.9.5 // indirect
354+	github.com/smallstep/certificates v0.28.2 // indirect
355+	github.com/smallstep/cli-utils v0.12.1 // indirect
356+	github.com/smallstep/linkedca v0.23.0 // indirect
357+	github.com/smallstep/nosql v0.7.0 // indirect
358+	github.com/smallstep/pkcs7 v0.2.1 // indirect
359+	github.com/smallstep/scep v0.0.0-20250221100424-171a5fa4fb1b // indirect
360 	github.com/smallstep/truststore v0.13.0 // indirect
361 	github.com/soniakeys/quant v1.0.0 // indirect
362-	github.com/spf13/cast v1.4.1 // indirect
363-	github.com/spf13/cobra v1.8.0 // indirect
364-	github.com/spf13/pflag v1.0.5 // indirect
365-	github.com/stoewer/go-strcase v1.2.0 // indirect
366-	github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
367-	github.com/tidwall/btree v1.6.0 // indirect
368+	github.com/spf13/cast v1.7.1 // indirect
369+	github.com/spf13/cobra v1.9.1 // indirect
370+	github.com/spf13/pflag v1.0.6 // indirect
371+	github.com/stoewer/go-strcase v1.3.0 // indirect
372+	github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
373+	github.com/tidwall/btree v1.7.0 // indirect
374 	github.com/tidwall/gjson v1.17.0 // indirect
375 	github.com/tidwall/match v1.1.1 // indirect
376 	github.com/tidwall/pretty v1.2.1 // indirect
377 	github.com/tidwall/redcon v1.6.2 // indirect
378 	github.com/tidwall/sjson v1.2.5 // indirect
379 	github.com/tinylib/msgp v1.2.5 // indirect
380-	github.com/tklauser/go-sysconf v0.3.14 // indirect
381-	github.com/tklauser/numcpus v0.9.0 // indirect
382-	github.com/urfave/cli v1.22.14 // indirect
383-	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
384+	github.com/tklauser/go-sysconf v0.3.15 // indirect
385+	github.com/tklauser/numcpus v0.10.0 // indirect
386+	github.com/urfave/cli v1.22.16 // indirect
387+	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
388 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
389-	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
390 	github.com/xujiajun/mmap-go v1.0.1 // indirect
391 	github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235 // indirect
392 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
393-	github.com/zeebo/blake3 v0.2.3 // indirect
394-	go.etcd.io/bbolt v1.3.9 // indirect
395-	go.etcd.io/etcd/api/v3 v3.5.14 // indirect
396-	go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
397-	go.etcd.io/etcd/client/v3 v3.5.14 // indirect
398+	github.com/zeebo/blake3 v0.2.4 // indirect
399+	go.etcd.io/bbolt v1.4.0 // indirect
400+	go.etcd.io/etcd/api/v3 v3.5.19 // indirect
401+	go.etcd.io/etcd/client/pkg/v3 v3.5.19 // indirect
402+	go.etcd.io/etcd/client/v3 v3.5.19 // indirect
403 	go.opencensus.io v0.24.0 // indirect
404-	go.step.sm/cli-utils v0.9.0 // indirect
405-	go.step.sm/crypto v0.45.0 // indirect
406-	go.step.sm/linkedca v0.20.1 // indirect
407+	go.step.sm/crypto v0.59.1 // indirect
408 	go.uber.org/automaxprocs v1.6.0 // indirect
409-	go.uber.org/mock v0.4.0 // indirect
410+	go.uber.org/mock v0.5.0 // indirect
411 	go.uber.org/multierr v1.11.0 // indirect
412 	go.uber.org/zap v1.27.0 // indirect
413-	go.uber.org/zap/exp v0.2.0 // indirect
414-	golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
415-	golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
416+	go.uber.org/zap/exp v0.3.0 // indirect
417+	golang.org/x/crypto/x509roots/fallback v0.0.0-20250312005926-b369b723c8ad // indirect
418+	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
419 	golang.org/x/image v0.25.0 // indirect
420-	golang.org/x/mod v0.23.0 // indirect
421+	golang.org/x/mod v0.24.0 // indirect
422 	golang.org/x/net v0.37.0 // indirect
423+	golang.org/x/oauth2 v0.28.0 // indirect
424 	golang.org/x/sync v0.12.0 // indirect
425 	golang.org/x/sys v0.31.0 // indirect
426 	golang.org/x/term v0.30.0 // indirect
427 	golang.org/x/text v0.23.0 // indirect
428 	golang.org/x/time v0.11.0 // indirect
429-	golang.org/x/tools v0.30.0 // indirect
430-	google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
431-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
432-	google.golang.org/grpc v1.70.0 // indirect
433+	golang.org/x/tools v0.31.0 // indirect
434+	google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf // indirect
435+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
436+	google.golang.org/grpc v1.71.0 // indirect
437+	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
438 	gopkg.in/yaml.v3 v3.0.1 // indirect
439-	howett.net/plist v1.0.0 // indirect
440+	howett.net/plist v1.0.1 // indirect
441 	mvdan.cc/xurls/v2 v2.6.0 // indirect
442 )
M go.sum
+422, -446
   1@@ -1,56 +1,64 @@
   2+cel.dev/expr v0.22.0 h1:+hFFhLPmquBImfs1BiN2PZmkr5ASse2ZOuaxIs9e4R8=
   3+cel.dev/expr v0.22.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
   4 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
   5-cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
   6-cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
   7-cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
   8+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
   9+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
  10+cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
  11+cloud.google.com/go v0.118.2 h1:bKXO7RXMFDkniAAvvuMrAPtQ/VHrs9e7J5UT3yrGdTY=
  12+cloud.google.com/go v0.118.2/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M=
  13+cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
  14+cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
  15 cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
  16 cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
  17 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
  18 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
  19-cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
  20-cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
  21-cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
  22-cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
  23-cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
  24-cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
  25+cloud.google.com/go/iam v1.4.0 h1:ZNfy/TYfn2uh/ukvhp783WhnbVluqf/tzOaqVUPlIPA=
  26+cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4=
  27+cloud.google.com/go/kms v1.21.0 h1:x3EeWKuYwdlo2HLse/876ZrKjk2L5r7Uexfm8+p6mSI=
  28+cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk=
  29+cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg=
  30+cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs=
  31 codeberg.org/emersion/go-scfg v0.1.0 h1:6dnGU0ZI4gX+O5rMjwhoaySItzHG710eXL5TIQKl+uM=
  32 codeberg.org/emersion/go-scfg v0.1.0/go.mod h1:0nooW1ufBB4SlJEdTtiVN9Or+bnNM1icOkQ6Tbrq6O0=
  33-dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
  34-dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
  35+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
  36+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
  37+dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
  38+dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
  39+dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
  40+dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
  41 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
  42 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
  43+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
  44 git.sr.ht/~delthas/senpai v0.3.1-0.20250311003540-18f699aaf9b0 h1:Knm2mHQwLsh1svD15lE27Cr6BMV2wH2t0OKUoSCNhuY=
  45 git.sr.ht/~delthas/senpai v0.3.1-0.20250311003540-18f699aaf9b0/go.mod h1:RzVz1R7QRHGcRDnJTcr7AN/cD3rj9scdgvupkXTJLYk=
  46-git.sr.ht/~rockorager/vaxis v0.12.1-0.20250309233058-d6d466f8f9b1 h1:o8opVUAysn+cQAnSadL1BVMhr2YcdjynRzPBz4fa9q0=
  47-git.sr.ht/~rockorager/vaxis v0.12.1-0.20250309233058-d6d466f8f9b1/go.mod h1:RSNtZnMeIwpyQzgIEYo9EHJb8Wcl/RhFSxypLpD/ajg=
  48 git.sr.ht/~rockorager/vaxis v0.12.1-0.20250312161844-81636f76af83 h1:9eVqJxJzMdnpfqfKKjvEvNDpVg6sIBvbI4FdTjhHqx8=
  49 git.sr.ht/~rockorager/vaxis v0.12.1-0.20250312161844-81636f76af83/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
  50 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
  51 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
  52 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
  53-github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
  54+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
  55 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
  56 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
  57 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
  58-github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
  59-github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
  60-github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
  61-github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
  62-github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
  63+github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
  64+github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
  65+github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
  66+github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
  67 github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
  68-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
  69-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
  70+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
  71+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
  72 github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
  73 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
  74-github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
  75-github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
  76+github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
  77+github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
  78 github.com/RoaringBitmap/roaring v1.2.1/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
  79-github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
  80-github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
  81-github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
  82-github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
  83+github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
  84+github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
  85+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
  86+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
  87 github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
  88-github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
  89-github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
  90+github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
  91+github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
  92 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
  93 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
  94 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
  95@@ -58,16 +66,16 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
  96 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
  97 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
  98 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
  99-github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
 100-github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
 101-github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 102-github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 103-github.com/antlabs/stl v0.0.1 h1:TRD3csCrjREeLhLoQ/supaoCvFhNLBTNIwuRGrDIs6Q=
 104-github.com/antlabs/stl v0.0.1/go.mod h1:wvVwP1loadLG3cRjxUxK8RL4Co5xujGaZlhbztmUEqQ=
 105-github.com/antlabs/timer v0.0.11 h1:z75oGFLeTqJHMOcWzUPBKsBbQAz4Ske3AfqJ7bsdcwU=
 106-github.com/antlabs/timer v0.0.11/go.mod h1:JNV8J3yGvMKhCavGXgj9HXrVZkfdQyKCcqXBT8RdyuU=
 107-github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 108-github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 109+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 110+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
 111+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
 112+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 113+github.com/antlabs/stl v0.0.2 h1:sna1AXR5yIkNE9lWhCcKbheFJSVfCa3vugnGyakI79s=
 114+github.com/antlabs/stl v0.0.2/go.mod h1:kKrO4xrn9cfS1mJVo+/BqePZjAYMXqD0amGF2Ouq7ac=
 115+github.com/antlabs/timer v0.1.4 h1:MHdE00MDnNfhJCmqSOdLXs35uGNwfkMwfbynxrGmQ1c=
 116+github.com/antlabs/timer v0.1.4/go.mod h1:mpw4zlD5KVjstEyUDp43DGLWsY076Mdo4bS78NTseRE=
 117+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 118+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
 119 github.com/antoniomika/syncmap v1.0.0 h1:iFSfbQFQOvHZILFZF+hqWosO0no+W9+uF4y2VEyMKWU=
 120 github.com/antoniomika/syncmap v1.0.0/go.mod h1:fK2829foEYnO4riNfyUn0SHQZt4ue3DStYjGU+sJj38=
 121 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
 122@@ -81,36 +89,24 @@ github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o
 123 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
 124 github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU=
 125 github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
 126-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
 127-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
 128 github.com/aws/aws-sdk-go-v2/config v1.29.7 h1:71nqi6gUbAUiEQkypHQcNVSFJVUFANpSeUNShiwWX2M=
 129 github.com/aws/aws-sdk-go-v2/config v1.29.7/go.mod h1:yqJQ3nh2HWw/uxd56bicyvmDW4KSc+4wN6lL8pYjynU=
 130 github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4=
 131 github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE=
 132 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 h1:JO8pydejFKmGcUNiiwt75dzLHRWthkwApIvPoyUtXEg=
 133 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29/go.mod h1:adxZ9i9DRmB8zAT0pO0yGnsmu0geomp5a3uq5XpgOJ8=
 134-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.63 h1:cTR4L7zlqh2YJjOWF62sMCyJWhm9ItUN3h/eOKh0xlU=
 135-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.63/go.mod h1:ryx0BXDm9YKRus5qaDeKcMh+XiEQ5uok/mJHkuGg4to=
 136 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c=
 137 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A=
 138 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA=
 139 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4=
 140 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 141 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 142-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.33 h1:/frG8aV09yhCVSOEC2pzktflJJO48NwY3xntHBwxHiA=
 143-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.33/go.mod h1:8vwASlAcV366M+qxZnjNzCjeastk1Rt1bpSRaGZanGU=
 144 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 145 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
 146-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.1 h1:7SuukGpyIgF5EiAbf1dZRxP+xSnY1WjiHBjL08fjJeE=
 147-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.6.1/go.mod h1:k+Vce/8R28tSozjdWphkrNhK8zLmdS9RgiDNZl6p8Rw=
 148 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 h1:2scbY6//jy/s8+5vGrk7l1+UtHl0h9A4MjOO2k/TM2E=
 149 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14/go.mod h1:bRpZPHZpSe5YRHmPfK3h1M7UBFCn2szHzyx0rw04zro=
 150-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.14 h1:fgdkfsxTehqPcIQa24G/Omwv9RocTq2UcONNX/OnrZI=
 151-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.14/go.mod h1:wMxQ3OE8fiM8z2YRAeb2J8DLTTWMvRyYYuQOs26AbTQ=
 152-github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0=
 153-github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
 154-github.com/aws/aws-sdk-go-v2/service/s3 v1.77.1 h1:5bI9tJL2Z0FGFtp/LPDv0eyliFBHCn7LAhqpQuL+7kk=
 155-github.com/aws/aws-sdk-go-v2/service/s3 v1.77.1/go.mod h1:njj3tSJONkfdLt4y6X8pyqeM6sJLNZxmzctKKV+n1GM=
 156+github.com/aws/aws-sdk-go-v2/service/kms v1.37.18 h1:pi9M/9n1PLayBXjia7LfwgXwcpFdFO7Q2cqKOZa1ZmM=
 157+github.com/aws/aws-sdk-go-v2/service/kms v1.37.18/go.mod h1:vZXvmzfhdsPj/axc8+qk/2fSCP4hGyaZ1MAduWEHAxM=
 158 github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 h1:YV6xIKDJp6U7YB2bxfud9IENO1LRpGhe2Tv/OKtPrOQ=
 159 github.com/aws/aws-sdk-go-v2/service/sso v1.24.16/go.mod h1:DvbmMKgtpA6OihFJK13gHMZOZrCHttz8wPHGKXqU+3o=
 160 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 h1:kMyK3aKotq1aTBsj1eS8ERJLjqYRRRcsmP33ozlCvlk=
 161@@ -119,10 +115,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 h1:ht1jVmeeo2anR7zDiYJLSnRYnO/
 162 github.com/aws/aws-sdk-go-v2/service/sts v1.33.15/go.mod h1:xWZ5cOiFe3czngChE4LhCBqUxNwgfwndEF7XlYP/yD8=
 163 github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
 164 github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 165-github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 166-github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 167-github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
 168-github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
 169 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 170 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 171 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 172@@ -131,20 +123,25 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 173 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 174 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 175 github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 176-github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
 177-github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 178+github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 179+github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
 180+github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 181+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 182+github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
 183 github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU=
 184 github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw=
 185-github.com/buraksezer/olric v0.5.6 h1:fYvf+jMZ20igrxmnw+QdY3dKPQb3+6WUONCKkUEgnU8=
 186-github.com/buraksezer/olric v0.5.6/go.mod h1:ndjlnRvJfFrE8eJlQNBJsDJa11tIsb5BXSfPmTi7qjE=
 187+github.com/buraksezer/olric v0.5.7 h1:K8ypVViiPkXiqBz3UyDAY99cHvvofAR65fmH7ElPEWE=
 188+github.com/buraksezer/olric v0.5.7/go.mod h1:S1R+9Zt7P9TCbvQZvY/RYuRehLLRPDfbJNkukQsLJ4k=
 189 github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
 190 github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
 191-github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
 192-github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw=
 193-github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0=
 194-github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
 195+github.com/caddyserver/caddy/v2 v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
 196+github.com/caddyserver/caddy/v2 v2.9.1/go.mod h1:ImUELya2el1FDVp3ahnSO2iH1or1aHxlQEQxd/spP68=
 197+github.com/caddyserver/certmagic v0.22.0 h1:hi2skv2jouUw9uQUEyYSTTmqPZPHgf61dOANSIVCLOw=
 198+github.com/caddyserver/certmagic v0.22.0/go.mod h1:Vc0msarAPhOagbDc/SU6M2zbzdwVuZ0lkTh2EqtH4vs=
 199 github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
 200 github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
 201+github.com/ccoveille/go-safecast v1.6.0 h1:kxc0VIsdEaYoKZbDiGBZBV62zAp0RdtFNH6E3Krev8s=
 202+github.com/ccoveille/go-safecast v1.6.0/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
 203 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 204 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 205 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 206@@ -152,36 +149,6 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 207 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 208 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 209 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 210-github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
 211-github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
 212-github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI=
 213-github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw=
 214-github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
 215-github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
 216-github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
 217-github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
 218-github.com/charmbracelet/promwish v0.7.0 h1:oaMH+ey6W4DDIv1xucS8jL1ik/Q46qxjNXlh6XxEm+s=
 219-github.com/charmbracelet/promwish v0.7.0/go.mod h1:WbRJN9irg8LmsBU8G2rFF8md9O3rSg63qrnqquP/+cs=
 220-github.com/charmbracelet/ssh v0.0.0-20250213143314-8712ec3ff3ef h1:dNZwn4is5svUd+sQEGsrXtp7VwD2ipYaCkKMzcpAEIE=
 221-github.com/charmbracelet/ssh v0.0.0-20250213143314-8712ec3ff3ef/go.mod h1:hg+I6gvlMl16nS9ZzQNgBIrrCasGwEw0QiLsDcP01Ko=
 222-github.com/charmbracelet/wish v1.4.6 h1:27WRqMTUmyFoZASoaAaEe78Je7LTU4VqyoBxnl4d9XA=
 223-github.com/charmbracelet/wish v1.4.6/go.mod h1:RRy2LFW3WQ3tlPmMMGgEeSMDVlFd5yqklGBVZWQSHmk=
 224-github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
 225-github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
 226-github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
 227-github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
 228-github.com/charmbracelet/x/errors v0.0.0-20250226164017-59292a315e58 h1:UWrrJJrdFfi7Y5XNKjz8/1RtZzGbshaFEZzlI7CgJ7M=
 229-github.com/charmbracelet/x/errors v0.0.0-20250226164017-59292a315e58/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
 230-github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
 231-github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
 232-github.com/charmbracelet/x/input v0.3.1 h1:TE4s3fTRj+OUpJ86dKphrN99+NgBnto//EkWncMJQIg=
 233-github.com/charmbracelet/x/input v0.3.1/go.mod h1:4w9jS/NW62WrHSdmjbpzydvnbqkd+mtyK8WOWbHCdvs=
 234-github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
 235-github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
 236-github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
 237-github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
 238-github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
 239-github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
 240 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 241 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
 242 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
 243@@ -195,49 +162,45 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
 244 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 245 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 246 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 247-github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 248-github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 249 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
 250 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 251 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 252 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 253+github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
 254+github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 255 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 256 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 257 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 258-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 259-github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 260+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 261 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 262 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 263 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 264-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 265-github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
 266-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 267-github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 268+github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 269+github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 270+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 271 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 272-github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
 273-github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 274-github.com/darkweak/go-esi v0.0.5 h1:b9LHI8Tz46R+i6p8avKPHAIBRQUCZDebNmKm5w/Zrns=
 275-github.com/darkweak/go-esi v0.0.5/go.mod h1:koCJqwum1u6mslyZuq/Phm6hfG1K3ZK5Y7jrUBTH654=
 276+github.com/darkweak/go-esi v0.0.6 h1:eVHCJfqrZwOHPfRK7JTlSYG9F8lfpX/d4lz/41RQkd8=
 277+github.com/darkweak/go-esi v0.0.6/go.mod h1:IJSayeQZDUh5R5ayyDC3wUEBykti12aUa0eUxZZeodk=
 278 github.com/darkweak/souin v1.7.5 h1:drNhZc0GhSbGcugiGfcYdLDTcx3DCZW6o13wwRj5o5Y=
 279 github.com/darkweak/souin v1.7.5/go.mod h1:PcP+hhvYOdqn4OmeScKKvit0TihYVYS1o154mhfWT/s=
 280 github.com/darkweak/souin/plugins/souin/storages v1.7.5 h1:KlHnwecr1WVNdWJ2OfhBsY6OujWvzkomowjSoBAJgxY=
 281 github.com/darkweak/souin/plugins/souin/storages v1.7.5/go.mod h1:ok3cIKHdIWjMtM6EgC8fBwNJL9hWG3deyTwN+M0zA4g=
 282-github.com/darkweak/storages/badger v0.0.8 h1:rKVXrasVA74xgiqGRgW0kH11NUIlWwn9HiFyHUok85k=
 283-github.com/darkweak/storages/badger v0.0.8/go.mod h1:ZmrNmKkFzyu/B3+1nsvVeTvyg2I2mOV5yTpT46mZ06o=
 284-github.com/darkweak/storages/core v0.0.11 h1:IwvpAtkhOmxC5pIffJ8opW6erpTnIi5zqPveiAQs8ew=
 285-github.com/darkweak/storages/core v0.0.11/go.mod h1:ajTpB9IFLRIRY0EEFLjM5vtsrcNTh+TJK9yRxgG5/wY=
 286-github.com/darkweak/storages/etcd v0.0.8 h1:Guzv6zgxkQJLjak36KsbtQqkmwMRJoZZI0B7ztZKIik=
 287-github.com/darkweak/storages/etcd v0.0.8/go.mod h1:Yw9xJramKAzIRoC7tizVMYPSwUBHqxY5BPTh8OgyISY=
 288-github.com/darkweak/storages/nats v0.0.8 h1:HRS3i2zzzIq1Qb3yoOUWD6MoRQgGV1NbECF1ex4wZjg=
 289-github.com/darkweak/storages/nats v0.0.8/go.mod h1:ap1RYc9aQHYylUDVKh2G2KF2GTzxoB+oFk3tZ9lxJLE=
 290-github.com/darkweak/storages/nuts v0.0.8 h1:obxlGOyvOGZw6TGTAMVrYqbH46z5kuHaF2zjbumTNUk=
 291-github.com/darkweak/storages/nuts v0.0.8/go.mod h1:1vGrcWTRLMXamfubwyEfVf/AJA4L7DjYzezBko5ODPM=
 292-github.com/darkweak/storages/olric v0.0.8 h1:QSRIBb8IyBlt/wxh5DkGiSsaeE6SiRtJZeJy3dNf4nU=
 293-github.com/darkweak/storages/olric v0.0.8/go.mod h1:Qk7FK28K/ogIdneaZzoSLmTGfNlCxvvN7yhkEqjh/TE=
 294-github.com/darkweak/storages/otter v0.0.11 h1:j4uOAycRMZVVKZhrqxCJoOVezfPv3pHB5YQ6iX9aKQo=
 295-github.com/darkweak/storages/otter v0.0.11/go.mod h1:SI70zPux7Q//J+cFq3KOgRthBENXusziKK7ztV9vpDo=
 296-github.com/darkweak/storages/redis v0.0.8 h1:0CHLkImyaI/sYs+IOurYLAxFkrmz5dFblhfpF7oGhQc=
 297-github.com/darkweak/storages/redis v0.0.8/go.mod h1:pypJ5T3hweQWfHzFUjmZWeb1KaNK3ikNg1+rn0G+rD0=
 298+github.com/darkweak/storages/badger v0.0.13 h1:p54l0LGJUM33DaVWAc59B7RMqpWdVFOJLF1oOSioz7w=
 299+github.com/darkweak/storages/badger v0.0.13/go.mod h1:JegE7I+ThF+OMORiSX6od9Hgmf6r/g64EiOswFtqNNg=
 300+github.com/darkweak/storages/core v0.0.13 h1:T2nA306M9eXBEpIlSf8czCzXrJ4MAS62fJhOV7ijI9U=
 301+github.com/darkweak/storages/core v0.0.13/go.mod h1:3qJqrenCLpu+0bWPOAq36CmGpzL3SGWAz6KGZGnur1U=
 302+github.com/darkweak/storages/etcd v0.0.13 h1:oO5XfoOrSSvzM4X2vacFHCv2CNQvG9h4XVTizLcpGAU=
 303+github.com/darkweak/storages/etcd v0.0.13/go.mod h1:CfZVDHanzCNVCL3ZW52LPTIxnDoqQKXTRXGGOJTsmPw=
 304+github.com/darkweak/storages/nats v0.0.13 h1:WTKclCfKsgzGuvx4yodcyEryGDj8nMMXbwj7k6IwX7k=
 305+github.com/darkweak/storages/nats v0.0.13/go.mod h1:awloCHr4IGJrQMolc8d9kIkwUhgCpo9hYb23aTicDms=
 306+github.com/darkweak/storages/nuts v0.0.13 h1:skhTxSXcAfOBDJAk6KbIZyY9g+HqlmWPwGup7nPEL5c=
 307+github.com/darkweak/storages/nuts v0.0.13/go.mod h1:lj62qLEtxwMnrR+JhFSJGfShbBZMBRZ7d870Ia36AqU=
 308+github.com/darkweak/storages/olric v0.0.13 h1:rmCsisM1nNl248whoAP+f4qP73e8wREBsn2r6RbQ37Y=
 309+github.com/darkweak/storages/olric v0.0.13/go.mod h1:disvKQFUbpJ/TMl7w6Fb08Ulhzil0Mr4QKVNi4K99HI=
 310+github.com/darkweak/storages/otter v0.0.13 h1:sH2wukx9W5gnH/NR9R+NJAAP7yhQ0lHmwtYizPm8jfw=
 311+github.com/darkweak/storages/otter v0.0.13/go.mod h1:heF8STOMydF+u7QwE1guEfNheMhHCsjv+3LJFRj+HGs=
 312+github.com/darkweak/storages/redis v0.0.13 h1:AI0El75BaNDY+tVtsqu67VUV9gSTJSckB4HbyLdASfc=
 313+github.com/darkweak/storages/redis v0.0.13/go.mod h1:87/MeVNG/u0a0k8cwAQcobyoeKeNjKgS7PMg7uXNPbE=
 314 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 315 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 316 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 317@@ -254,19 +217,20 @@ github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0
 318 github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
 319 github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 320 github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 321-github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
 322 github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
 323+github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
 324+github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
 325 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 326-github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
 327-github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 328+github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
 329+github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 330 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 331 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 332 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
 333 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
 334 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 335 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 336-github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
 337-github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 338+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
 339+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 340 github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
 341 github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
 342 github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk=
 343@@ -300,25 +264,30 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
 344 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 345 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 346 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 347-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
 348-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 349 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 350 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 351 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 352-github.com/forPelevin/gomoji v1.2.0 h1:9k4WVSSkE1ARO/BWywxgEUBvR/jMnao6EZzrql5nxJ8=
 353-github.com/forPelevin/gomoji v1.2.0/go.mod h1:8+Z3KNGkdslmeGZBC3tCrwMrcPy5GRzAD+gL9NAwMXg=
 354+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 355+github.com/forPelevin/gomoji v1.3.0 h1:WPIOLWB1bvRYlKZnSSEevLt3IfKlLs+tK+YA9fFYlkE=
 356+github.com/forPelevin/gomoji v1.3.0/go.mod h1:mM6GtmCgpoQP2usDArc6GjbXrti5+FffolyQfGgPboQ=
 357+github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
 358+github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
 359+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 360+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 361 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 362 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 363 github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
 364 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 365-github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
 366-github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
 367+github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
 368+github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
 369+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 370 github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8=
 371 github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
 372 github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
 373 github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
 374 github.com/gkampitakis/go-snaps v0.5.7 h1:uVGjHR4t4pPHU944udMx7VKHpwepZXmvDMF+yDmI0rg=
 375 github.com/gkampitakis/go-snaps v0.5.7/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y=
 376+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 377 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 378 github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 379 github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 380@@ -326,22 +295,16 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
 381 github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 382 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 383 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 384-github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 385-github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 386-github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 387+github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
 388+github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 389+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
 390+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 391 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 392 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 393-github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
 394-github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
 395 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
 396-github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
 397-github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
 398 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 399 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 400 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 401-github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 402-github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
 403-github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 404 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 405 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 406 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 407@@ -351,25 +314,23 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 408 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
 409 github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 410 github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 411-github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 412 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 413-github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 414+github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
 415+github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
 416 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 417 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 418-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 419-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 420+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 421+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 422 github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 423-github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
 424-github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 425+github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
 426+github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 427 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 428 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 429 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 430 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 431 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 432-github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 433-github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 434-github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
 435-github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 436+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
 437+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
 438 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 439 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 440 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 441@@ -380,13 +341,13 @@ github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgR
 442 github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
 443 github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
 444 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 445-github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
 446-github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 447 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 448 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 449-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 450-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 451+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
 452+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 453+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
 454 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 455+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 456 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 457 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 458 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 459@@ -403,18 +364,18 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
 460 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 461 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 462 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 463-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 464-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 465+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
 466+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 467 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 468-github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 469-github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 470-github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
 471-github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
 472+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 473+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 474+github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
 475+github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
 476 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
 477 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
 478 github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 479-github.com/google/flatbuffers v23.1.21+incompatible h1:bUqzx/MXCDxuS0hRJL2EfjyZL3uQrPbMocUa8zGqsTA=
 480-github.com/google/flatbuffers v23.1.21+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 481+github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
 482+github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 483 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 484 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 485 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 486@@ -424,35 +385,44 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 487 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 488 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 489 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 490+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 491 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 492 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 493-github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
 494-github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
 495-github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
 496-github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
 497+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 498+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 499+github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
 500+github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
 501+github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k=
 502+github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8=
 503 github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
 504 github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
 505 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 506+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 507+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 508 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 509-github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
 510-github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 511-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 512+github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
 513+github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 514 github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
 515 github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 516-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 517 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 518 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 519 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 520 github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
 521 github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
 522+github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
 523+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
 524+github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
 525 github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
 526 github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
 527+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 528 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
 529 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
 530 github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
 531 github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
 532 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
 533 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 534+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 535+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 536 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 537 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 538 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 539@@ -460,104 +430,72 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
 540 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 541 github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
 542 github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 543+github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
 544+github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
 545 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 546-github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
 547-github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 548+github.com/hashicorp/go-msgpack/v2 v2.1.3 h1:cB1w4Zrk0O3jQBTcFMKqYQWRFfsSQ/TYKNyUUVyCP2c=
 549+github.com/hashicorp/go-msgpack/v2 v2.1.3/go.mod h1:SjlwKKFnwBXvxD/I1bEcfJIBbEJ+MCUn39TxymNR5ZU=
 550 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 551 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 552 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 553 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
 554 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 555-github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
 556 github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
 557-github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
 558+github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
 559+github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
 560 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 561+github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
 562+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 563 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 564-github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
 565-github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 566+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
 567+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 568 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 569 github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
 570 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 571-github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
 572 github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
 573+github.com/hashicorp/memberlist v0.5.3 h1:tQ1jOCypD0WvMemw/ZhhtH+PWpzcftQvgCorLu0hndk=
 574+github.com/hashicorp/memberlist v0.5.3/go.mod h1:h60o12SZn/ua/j0B6iKAZezA4eDaGsIuPO70eOaJ6WE=
 575 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 576 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 577 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 578-github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
 579-github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 580+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
 581+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 582 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 583-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 584-github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
 585-github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 586 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 587 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 588 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 589-github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
 590-github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
 591-github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
 592-github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
 593-github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
 594-github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
 595-github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
 596-github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
 597-github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
 598-github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
 599-github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
 600-github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
 601-github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
 602-github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
 603-github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
 604-github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
 605-github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
 606-github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
 607 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 608 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 609-github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
 610-github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
 611-github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
 612-github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 613-github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 614-github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 615-github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 616-github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
 617-github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 618-github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
 619-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
 620-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 621-github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
 622-github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
 623-github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
 624-github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
 625-github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
 626-github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
 627-github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
 628-github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
 629-github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 630-github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
 631-github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
 632-github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
 633-github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 634-github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 635-github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 636+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
 637+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 638+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
 639+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
 640+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
 641+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 642+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 643 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 644 github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
 645 github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
 646+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 647 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 648 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 649+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 650+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 651 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 652 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 653+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 654 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 655+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 656 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 657 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 658 github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 659 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 660 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 661 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 662-github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 663 github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
 664 github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 665 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 666-github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 667+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 668 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 669 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 670 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 671@@ -566,25 +504,21 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
 672 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 673 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 674 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 675-github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
 676+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 677 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 678 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 679 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 680 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 681 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 682-github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 683-github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 684-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 685-github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 686 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 687 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 688-github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
 689-github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
 690-github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 691-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 692-github.com/lufia/plan9stats v0.0.0-20250224150550-a661cff19cfb h1:YU0XAr3+rMpM8fP80KEesn32Qa9qkbquokvuwzWyYuA=
 693-github.com/lufia/plan9stats v0.0.0-20250224150550-a661cff19cfb/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
 694+github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8=
 695+github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
 696+github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d h1:fjMbDVUGsMQiVZnSQsmouYJvMdwsGiDipOZoN66v844=
 697+github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
 698+github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 699 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 700+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 701 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 702 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 703 github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
 704@@ -592,19 +526,11 @@ github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRH
 705 github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
 706 github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 707 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 708-github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 709-github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 710-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 711-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 712+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
 713+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 714 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 715-github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 716-github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 717-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 718-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 719 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 720 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 721-github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 722-github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 723 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 724 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 725 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 726@@ -615,12 +541,13 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU
 727 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 728 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 729 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 730-github.com/maypok86/otter v1.2.1 h1:xyvMW+t0vE1sKt/++GTkznLitEl7D/msqXkAbLwiC1M=
 731-github.com/maypok86/otter v1.2.1/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
 732+github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
 733+github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
 734 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
 735 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 736-github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
 737-github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
 738+github.com/mholt/acmez/v3 v3.1.0 h1:RlOx2SSZ8dIAM5GfkMe8TdaxjjkiHTGorlMUt8GeMzg=
 739+github.com/mholt/acmez/v3 v3.1.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
 740+github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 741 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
 742 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
 743 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 744@@ -629,14 +556,13 @@ github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
 745 github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
 746 github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
 747 github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
 748-github.com/minio/madmin-go/v3 v3.0.94 h1:n2S8zgm0eRJ09YC7qrDTFDQDQOEUzCTiKEPywCitO/s=
 749-github.com/minio/madmin-go/v3 v3.0.94/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE=
 750+github.com/minio/madmin-go/v3 v3.0.97 h1:P7XO+ofPexkXy9akxG9Xoif1zIkbmWKeKtG3AquVzEY=
 751+github.com/minio/madmin-go/v3 v3.0.97/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE=
 752 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 753 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
 754-github.com/minio/minio-go/v7 v7.0.87 h1:nkr9x0u53PespfxfUqxP3UYWiE2a41gaofgNnC4Y8WQ=
 755-github.com/minio/minio-go/v7 v7.0.87/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg=
 756+github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs=
 757+github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg=
 758 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 759-github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
 760 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 761 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 762 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 763@@ -644,7 +570,6 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
 764 github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
 765 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 766 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 767-github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 768 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 769 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 770 github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
 771@@ -662,21 +587,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 772 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 773 github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
 774 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
 775-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
 776-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 777-github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 778-github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 779-github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
 780-github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 781 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 782 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 783 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 784-github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU=
 785-github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
 786-github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
 787-github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
 788+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 789+github.com/nats-io/nats.go v1.39.1 h1:oTkfKBmz7W047vRxV762M67ZdXeOtUgvbBaNoQ+3PPk=
 790+github.com/nats-io/nats.go v1.39.1/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM=
 791+github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
 792+github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
 793 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 794 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 795+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
 796+github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
 797 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577 h1:hVmVNttSLNloGsbFKVXAUHonXTd8KKrv30U/8UkloKI=
 798 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577/go.mod h1:G3Cu1AW+dmRLDFpOi8eUAfc3cGoRHUjTkGjeRcndgl4=
 799 github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e h1:76Dng5ms0fR+26doKZAvNqhi2UPfnLxGfPIDEr+BBlM=
 800@@ -692,14 +614,15 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
 801 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 802 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 803 github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 804-github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
 805-github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
 806+github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ=
 807+github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
 808 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 809 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 810 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 811 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
 812-github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
 813-github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
 814+github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
 815+github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
 816+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 817 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 818 github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
 819 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 820@@ -710,25 +633,19 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh
 821 github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
 822 github.com/picosh/go-rsync-receiver v0.0.0-20250304201040-fcc11dd22d79 h1:MyB9P43hlQ6A2FoP9LGeiTBL3WKToW4gcWd6lQPg/Zg=
 823 github.com/picosh/go-rsync-receiver v0.0.0-20250304201040-fcc11dd22d79/go.mod h1:4ZICsr6bESoHP8He9DqROlZiMw4hHHjcbDzhtTTDQzA=
 824-github.com/picosh/pobj v0.0.0-20250304201248-a9c7179aa49b h1:lOsbgawiv0DXWcQQzey4TOY3Y/WTwgNpyUa4kneiKwI=
 825-github.com/picosh/pobj v0.0.0-20250304201248-a9c7179aa49b/go.mod h1:R6NE9t8BYGpU+n03geGNnZZC0REa99epFioDtnx6c0Y=
 826 github.com/picosh/pubsub v0.0.0-20241114191831-ec8f16c0eb88 h1:hdxE6rquHHw1/eeqS1b+ojLaxGtN8zOiTUclPwaVbPg=
 827 github.com/picosh/pubsub v0.0.0-20241114191831-ec8f16c0eb88/go.mod h1:+9hDKIDHQCvGFigCVlIl589BwpT9R4boKhUVc/OgRU4=
 828-github.com/picosh/send v0.0.0-20250304201154-e36cd3bbbb35 h1:qC8Rqijwi0I4gC+zIEAyJr46J6MSb4FdgzbnbMhGMgY=
 829-github.com/picosh/send v0.0.0-20250304201154-e36cd3bbbb35/go.mod h1:8330Tuhd4sBd7bDVxzcO47kORSq/aCHo+DEURecjj24=
 830-github.com/picosh/tunkit v0.0.0-20240905223921-532404cef9d9 h1:g5oZmnDFr11HarA8IAXcc4o9PBlolSM59QIATCSoato=
 831-github.com/picosh/tunkit v0.0.0-20240905223921-532404cef9d9/go.mod h1:UrDH/VCIc1wg/L6iY2zSYt4TiGw+25GsKSnkVkU40Dw=
 832 github.com/picosh/utils v0.0.0-20241120033529-8ca070c09bf4 h1:pwbgY9shKyMlpYvpUalTyV0ZVd5paj8pSEYT4OPOYTk=
 833 github.com/picosh/utils v0.0.0-20241120033529-8ca070c09bf4/go.mod h1:HogYEyJ43IGXrOa3D/kjM1pkzNAyh+pejRyv8Eo//pk=
 834-github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
 835-github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 836+github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
 837+github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 838 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 839 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 840 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 841 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 842 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 843-github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
 844-github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
 845+github.com/pkg/sftp v1.13.8 h1:Xt7eJ/xqXv7s0VuzFw7JXhZj6Oc1zI6l4GK8KP9sFB0=
 846+github.com/pkg/sftp v1.13.8/go.mod h1:DmvEkvKE2lshEeuo2JMp06yqcx9HVnR7e3zqQl42F3U=
 847 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 848 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 849 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 850@@ -739,49 +656,54 @@ github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEn
 851 github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
 852 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 853 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 854+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 855 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 856 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 857 github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 858-github.com/prometheus/client_golang v1.21.0-rc.0 h1:bR+RxBlwcr4q8hXkgSOA/J18j6n0/qH0Gb0DH+8c+RY=
 859-github.com/prometheus/client_golang v1.21.0-rc.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
 860+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 861+github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 862+github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
 863+github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
 864 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 865 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 866 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 867 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 868 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 869 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 870+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 871 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 872 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 873+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 874+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 875 github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
 876 github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
 877+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 878 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 879 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 880 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 881+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 882+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 883 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 884 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 885 github.com/prometheus/prom2json v1.4.1 h1:7McxdrHgPEOtMwWjkKtd0v5AhpR2Q6QAnlHKVxq0+tQ=
 886 github.com/prometheus/prom2json v1.4.1/go.mod h1:CzOQykSKFxXuC7ELUZHOHQvwKesQ3eN0p2PWLhFitQM=
 887 github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag=
 888 github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg=
 889-github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
 890-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
 891-github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
 892-github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
 893-github.com/redis/rueidis v1.0.39 h1:RNMbL7/tMkiVga/0ukbbFFslcPQckq4zs7c81mkIfTk=
 894-github.com/redis/rueidis v1.0.39/go.mod h1:bnbkk4+CkXZgDPEbUtSos/o55i4RhFYYesJ4DS2zmq0=
 895+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
 896+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
 897+github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
 898+github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
 899+github.com/redis/rueidis v1.0.55 h1:PrRv6eETcanBgYVNdwxn6RyUaPfxN6H+b5jUA4mfpkw=
 900+github.com/redis/rueidis v1.0.55/go.mod h1:cr7ILwt1AqyMRfjWlA9Orubj6gp1xzn1DPyhmrhv/x0=
 901 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 902 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 903 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 904 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 905-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 906 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 907 github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
 908 github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
 909-github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 910 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
 911 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 912-github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 913-github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
 914 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 915 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 916 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 917@@ -790,7 +712,6 @@ github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDj
 918 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
 919 github.com/safchain/ethtool v0.5.10 h1:Im294gZtuf4pSGJRAOGKaASNi3wMeFaGaWuSaomedpc=
 920 github.com/safchain/ethtool v0.5.10/go.mod h1:w9jh2Lx7YBR4UwzLkzCmWl85UY0W2uZdd7/DckVE5+c=
 921-github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 922 github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
 923 github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
 924 github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
 925@@ -802,66 +723,93 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
 926 github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
 927 github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
 928 github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
 929+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 930 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
 931 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
 932 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
 933 github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
 934 github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
 935 github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 936-github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
 937-github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 938-github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 939+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 940+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 941+github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
 942+github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
 943+github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
 944+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
 945+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
 946+github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
 947+github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
 948+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
 949+github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
 950+github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
 951+github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
 952+github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
 953+github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
 954+github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
 955+github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
 956+github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
 957+github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
 958+github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
 959+github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
 960+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 961 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 962 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 963+github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
 964+github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
 965 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d h1:4FkGkGts6gLznca6fgclIvbupwbq543mb/fFkog4VIg=
 966 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d/go.mod h1:fTTj1EOmRdtuwYw3jF/1X2dTa0N1BdbZhrpA21N/S4I=
 967 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 968-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 969 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 970+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 971 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 972 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 973 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 974-github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
 975-github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI=
 976+github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY=
 977+github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
 978 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
 979 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
 980-github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o=
 981-github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis=
 982+github.com/smallstep/certificates v0.28.2 h1:KN7Ytxatoa35CnuDKw/tX49SF+2f6ilB2vyMqIS1mtc=
 983+github.com/smallstep/certificates v0.28.2/go.mod h1:UWVIqq5A3toV9rSzYUnimqRNeU6xCt3YWB2mTaCxm28=
 984+github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE=
 985+github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
 986 github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
 987 github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
 988-github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y=
 989-github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y=
 990-github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg=
 991-github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
 992-github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw=
 993-github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
 994+github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU=
 995+github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8=
 996+github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
 997+github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
 998+github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
 999+github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
1000+github.com/smallstep/scep v0.0.0-20250221100424-171a5fa4fb1b h1:A1pVOceKLBY+QT10XcMFMWTaxct5zFjwIFMnDk13dhA=
1001+github.com/smallstep/scep v0.0.0-20250221100424-171a5fa4fb1b/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
1002 github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
1003 github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
1004 github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y=
1005 github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds=
1006+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
1007+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
1008 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
1009 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
1010 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
1011 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
1012 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
1013-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
1014-github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
1015-github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
1016+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
1017+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
1018 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
1019-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
1020-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
1021+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
1022+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
1023 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
1024 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
1025-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
1026-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
1027+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
1028+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
1029 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
1030-github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
1031-github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
1032+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
1033+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
1034 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1035 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1036-github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
1037 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
1038 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
1039+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
1040 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
1041 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
1042 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
1043@@ -872,13 +820,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
1044 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
1045 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
1046 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
1047+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1048 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
1049 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1050-github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU=
1051-github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
1052+github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
1053+github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
1054+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
1055 github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
1056-github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
1057-github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
1058+github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
1059+github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
1060 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
1061 github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
1062 github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
1063@@ -893,22 +843,23 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
1064 github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
1065 github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
1066 github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
1067-github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
1068-github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
1069-github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
1070-github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
1071+github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
1072+github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
1073+github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
1074+github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
1075 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
1076 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
1077-github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
1078-github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
1079-github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
1080+github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
1081+github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
1082+github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
1083+github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
1084 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
1085+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
1086+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
1087 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
1088 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
1089-github.com/x-way/crawlerdetect v0.2.24 h1:kZDzSeiXB64M+Bknopn5GddHT+LBocD61jEjqDOufLE=
1090-github.com/x-way/crawlerdetect v0.2.24/go.mod h1:s6iUJZPq/WNBJThPRK+zk8ah7iIbGUZn9nYWMls3YP0=
1091-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
1092-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
1093+github.com/x-way/crawlerdetect v0.2.28 h1:3G88EmiFv6+xXFGOv98s4GQaGzIp2Go2/03LVg+aIW8=
1094+github.com/x-way/crawlerdetect v0.2.28/go.mod h1:UNantmBXjcJBve2lS8YN2XlgHmR6MKY8abq/AyTJI9g=
1095 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
1096 github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
1097 github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
1098@@ -929,32 +880,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
1099 github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
1100 github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
1101 github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
1102-github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
1103-github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
1104+github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
1105+github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
1106 github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
1107 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
1108-github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
1109-go.abhg.dev/goldmark/anchor v0.1.1 h1:NUH3hAzhfeymRqZKOkSoFReZlEAmfXBZlbXEzpD2Qgc=
1110-go.abhg.dev/goldmark/anchor v0.1.1/go.mod h1:zYKiaHXTdugwVJRZqInVdmNGQRM3ZRJ6AGBC7xP7its=
1111+go.abhg.dev/goldmark/anchor v0.2.0 h1:RQZTodRc6VHSUoQYKFlyH0pokbhk1klwUuGgDmjGp2E=
1112+go.abhg.dev/goldmark/anchor v0.2.0/go.mod h1:Ym74zBV+QBKxK9ITOty680N9FT8otgGYvtYXroJUWms=
1113 go.abhg.dev/goldmark/hashtag v0.3.1 h1:k0FQwEtVQ1SstIRR2fqDJ4VNYUS0AXLp869V0qHOZMg=
1114 go.abhg.dev/goldmark/hashtag v0.3.1/go.mod h1:rXtvxXPL7auhPMGRdG02UrXn/9LMm6PNdP5HO64zbVU=
1115-go.abhg.dev/goldmark/toc v0.10.0 h1:de3LrIimwtGhBMKh7aEl1c6n4XWwOdukIO5wOAMYZzg=
1116-go.abhg.dev/goldmark/toc v0.10.0/go.mod h1:OpH0qqRP9v/eosCV28ZeqGI78jZ8rri3C7Jh8fzEo2M=
1117-go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
1118-go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
1119-go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
1120-go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
1121-go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
1122-go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
1123-go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
1124-go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk=
1125+go.abhg.dev/goldmark/toc v0.11.0 h1:IRixVy3/yVPKvFBc37EeBPi8XLTXrtH6BYaonSjkF8o=
1126+go.abhg.dev/goldmark/toc v0.11.0/go.mod h1:XMFIoI1Sm6dwF9vKzVDOYE/g1o5BmKXghLG8q/wJNww=
1127+go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
1128+go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
1129+go.etcd.io/etcd/api/v3 v3.5.19 h1:w3L6sQZGsWPuBxRQ4m6pPP3bVUtV8rjW033EGwlr0jw=
1130+go.etcd.io/etcd/api/v3 v3.5.19/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U=
1131+go.etcd.io/etcd/client/pkg/v3 v3.5.19 h1:9VsyGhg0WQGjDWWlDI4VuaS9PZJGNbPkaHEIuLwtixk=
1132+go.etcd.io/etcd/client/pkg/v3 v3.5.19/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0=
1133+go.etcd.io/etcd/client/v3 v3.5.19 h1:+4byIz6ti3QC28W0zB0cEZWwhpVHXdrKovyycJh1KNo=
1134+go.etcd.io/etcd/client/v3 v3.5.19/go.mod h1:FNzyinmMIl0oVsty1zA3hFeUrxXI/JpEnz4sG+POzjU=
1135+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
1136 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
1137 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
1138 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
1139 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
1140 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
1141-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
1142-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
1143+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
1144+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
1145 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
1146 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
1147 go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
1148@@ -963,89 +914,79 @@ go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS
1149 go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
1150 go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
1151 go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
1152-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
1153-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
1154+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
1155+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
1156 go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
1157 go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
1158-go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
1159-go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
1160-go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
1161-go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY=
1162-go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
1163-go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
1164-go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
1165-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
1166-go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
1167-go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
1168+go.step.sm/crypto v0.59.1 h1:jUL+5p19YS9YJKLaPUgkS2OdGm7s0+hwP7AqTFyF9Cg=
1169+go.step.sm/crypto v0.59.1/go.mod h1:XHavmnzfTyPpQE/n4YokEtjiBzP3LZI9/1O061f5y0o=
1170 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
1171 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
1172 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
1173 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
1174-go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
1175-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
1176-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
1177-go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
1178-go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
1179+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
1180+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
1181 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
1182 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
1183-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
1184-go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
1185-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
1186-go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
1187 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
1188 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
1189-go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
1190-go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
1191+go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
1192+go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
1193+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
1194+golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
1195 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
1196+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
1197 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
1198 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
1199-golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
1200-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
1201-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
1202+golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
1203 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
1204 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
1205 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
1206 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
1207-golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
1208-golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
1209-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
1210 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
1211-golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
1212-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
1213+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
1214 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
1215+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
1216+golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
1217+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
1218+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
1219 golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
1220 golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
1221-golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
1222-golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
1223+golang.org/x/crypto/x509roots/fallback v0.0.0-20250312005926-b369b723c8ad h1:FujBcZXke9mOYht0C528NPgcM3mW5yqv8Nc07SGpvTk=
1224+golang.org/x/crypto/x509roots/fallback v0.0.0-20250312005926-b369b723c8ad/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
1225 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
1226-golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
1227-golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
1228+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
1229+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
1230 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
1231 golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
1232 golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
1233+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
1234 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
1235 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
1236 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
1237-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
1238-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
1239-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
1240 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
1241 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
1242 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
1243 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
1244 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
1245-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
1246-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
1247+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
1248+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
1249+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
1250+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
1251+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
1252 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1253 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1254 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1255+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1256+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1257 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1258+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1259 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
1260 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
1261+golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
1262 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
1263 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1264 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1265-golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1266 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1267 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1268 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1269@@ -1054,6 +995,7 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/
1270 golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
1271 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
1272 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
1273+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
1274 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
1275 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
1276 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
1277@@ -1061,15 +1003,22 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
1278 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
1279 golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
1280 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
1281-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
1282 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
1283-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
1284 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
1285+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
1286+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
1287+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
1288+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
1289+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
1290 golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
1291 golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
1292 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
1293-golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
1294-golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
1295+golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
1296+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
1297+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
1298+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
1299+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
1300+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
1301 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1302 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1303 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1304@@ -1077,27 +1026,33 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
1305 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1306 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1307 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1308+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1309 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1310 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1311 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1312+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
1313+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1314+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1315+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1316+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1317+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1318 golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
1319 golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
1320 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1321 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1322 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1323 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1324+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1325 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1326 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1327 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1328 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1329 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1330-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1331-golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1332+golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1333 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1334 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1335 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1336 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1337-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1338 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1339 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1340 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1341@@ -1106,113 +1061,134 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
1342 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1343 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1344 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1345-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1346+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1347 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1348-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1349 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1350 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1351+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1352+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1353 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1354 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1355 golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1356 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1357+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1358 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1359 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1360 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1361+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1362 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1363 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1364-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1365 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1366 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1367 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1368 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1369 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1370-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1371 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1372 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1373-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1374 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1375 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1376-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1377 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1378-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1379+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1380 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1381+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1382+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1383+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1384 golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1385+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1386 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
1387 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
1388-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
1389+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
1390 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
1391 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
1392-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
1393 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
1394-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
1395 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
1396-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
1397+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
1398 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
1399+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
1400+golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
1401+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
1402+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
1403 golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
1404 golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
1405 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
1406+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
1407 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
1408 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
1409-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
1410 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
1411 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
1412-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
1413 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
1414 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
1415+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
1416 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
1417+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
1418+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
1419+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
1420+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
1421 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
1422 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
1423+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
1424+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
1425 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
1426 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
1427+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
1428 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
1429+golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
1430 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
1431 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
1432 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
1433-golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
1434 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
1435-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
1436-golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
1437 golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
1438-golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
1439-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
1440 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
1441-golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
1442 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
1443 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
1444 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
1445 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
1446 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
1447 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
1448-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
1449-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
1450-golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
1451-golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
1452+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
1453+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
1454+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
1455+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
1456 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
1457 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
1458 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
1459 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
1460-google.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA=
1461-google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M=
1462+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
1463+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
1464+google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
1465+google.golang.org/api v0.223.0 h1:JUTaWEriXmEy5AhvdMgksGGPEFsYfUKaPEYXd4c3Wvc=
1466+google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M=
1467 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
1468+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
1469+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
1470 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
1471 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
1472+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
1473+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
1474+google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
1475+google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
1476 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
1477 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
1478 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
1479-google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
1480-google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
1481-google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
1482-google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
1483-google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
1484-google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
1485+google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s=
1486+google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE=
1487+google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf h1:BdIVRm+fyDUn8lrZLPSlBCfM/YKDwUBYgDoLv9+DYo0=
1488+google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
1489+google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
1490+google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
1491+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
1492+google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
1493+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
1494 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
1495 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
1496 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
1497 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
1498 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
1499 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
1500-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
1501-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
1502+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
1503+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
1504+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
1505+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
1506 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
1507 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
1508 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
1509@@ -1228,15 +1204,11 @@ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwl
1510 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
1511 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
1512 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1513-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1514 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1515 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
1516 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
1517-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
1518 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
1519-gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
1520-gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
1521-gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
1522+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
1523 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
1524 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
1525 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
1526@@ -1251,12 +1223,16 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
1527 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1528 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1529 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1530+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
1531+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
1532 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
1533+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
1534 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
1535-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
1536-howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
1537-howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
1538+howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
1539+howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
1540 mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
1541 mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
1542 pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
1543 pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
1544+sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
1545+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
D pastes/ssh.go
+0, -105
  1@@ -1,105 +0,0 @@
  2-package pastes
  3-
  4-import (
  5-	"context"
  6-	"fmt"
  7-	"os"
  8-	"os/signal"
  9-	"syscall"
 10-	"time"
 11-
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15-	"github.com/picosh/pico/db/postgres"
 16-	"github.com/picosh/pico/filehandlers"
 17-	"github.com/picosh/pico/shared"
 18-	wsh "github.com/picosh/pico/wish"
 19-	"github.com/picosh/send/auth"
 20-	"github.com/picosh/send/list"
 21-	"github.com/picosh/send/pipe"
 22-	wishrsync "github.com/picosh/send/protocols/rsync"
 23-	"github.com/picosh/send/protocols/scp"
 24-	"github.com/picosh/send/proxy"
 25-	"github.com/picosh/utils"
 26-)
 27-
 28-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 29-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 30-		return []wish.Middleware{
 31-			pipe.Middleware(handler, ""),
 32-			list.Middleware(handler),
 33-			scp.Middleware(handler),
 34-			wishrsync.Middleware(handler),
 35-			auth.Middleware(handler),
 36-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 37-			wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool),
 38-		}
 39-	}
 40-}
 41-
 42-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 43-	return func(server *ssh.Server) error {
 44-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 45-
 46-		for name, subsystemHandlers := range server.SubsystemHandlers {
 47-			newSubsystemHandlers[name] = func(s ssh.Session) {
 48-				wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool)(ssh.Handler(subsystemHandlers))
 49-			}
 50-		}
 51-
 52-		server.SubsystemHandlers = newSubsystemHandlers
 53-
 54-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 55-	}
 56-}
 57-
 58-func StartSshServer() {
 59-	host := utils.GetEnv("PASTES_HOST", "0.0.0.0")
 60-	port := utils.GetEnv("PASTES_SSH_PORT", "2222")
 61-	promPort := utils.GetEnv("PASTES_PROM_PORT", "9222")
 62-	cfg := NewConfigSite("pastes-ssh")
 63-	logger := cfg.Logger
 64-	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 65-	defer dbh.Close()
 66-	hooks := &FileHooks{
 67-		Cfg: cfg,
 68-		Db:  dbh,
 69-	}
 70-
 71-	fileMap := map[string]filehandlers.ReadWriteHandler{
 72-		"fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 73-	}
 74-	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 75-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 76-	s, err := wish.NewServer(
 77-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 78-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 79-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
 80-		withProxy(
 81-			handler,
 82-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pastes-ssh"),
 83-		),
 84-	)
 85-	if err != nil {
 86-		logger.Error(err.Error())
 87-		return
 88-	}
 89-
 90-	done := make(chan os.Signal, 1)
 91-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 92-	logger.Info("Starting SSH server", "host", host, "port", port)
 93-	go func() {
 94-		if err = s.ListenAndServe(); err != nil {
 95-			logger.Error(err.Error())
 96-		}
 97-	}()
 98-
 99-	<-done
100-	logger.Info("Stopping SSH server")
101-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
102-	defer func() { cancel() }()
103-	if err := s.Shutdown(ctx); err != nil {
104-		logger.Error(err.Error())
105-	}
106-}
D pgs/ssh.go
+0, -129
  1@@ -1,129 +0,0 @@
  2-package pgs
  3-
  4-import (
  5-	"context"
  6-	"fmt"
  7-	"os"
  8-	"os/signal"
  9-	"syscall"
 10-	"time"
 11-
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15-	"github.com/picosh/pico/shared"
 16-	wsh "github.com/picosh/pico/wish"
 17-	"github.com/picosh/send/auth"
 18-	"github.com/picosh/send/list"
 19-	"github.com/picosh/send/pipe"
 20-	wishrsync "github.com/picosh/send/protocols/rsync"
 21-	"github.com/picosh/send/protocols/scp"
 22-	"github.com/picosh/send/protocols/sftp"
 23-	"github.com/picosh/send/proxy"
 24-	"github.com/picosh/tunkit"
 25-	"github.com/picosh/utils"
 26-)
 27-
 28-func createRouter(handler *UploadAssetHandler) proxy.Router {
 29-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 30-		return []wish.Middleware{
 31-			pipe.Middleware(handler, ""),
 32-			list.Middleware(handler),
 33-			scp.Middleware(handler),
 34-			wishrsync.Middleware(handler),
 35-			auth.Middleware(handler),
 36-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 37-			WishMiddleware(handler),
 38-			wsh.LogMiddleware(handler.GetLogger(s), handler.Cfg.DB),
 39-		}
 40-	}
 41-}
 42-
 43-func withProxy(handler *UploadAssetHandler, otherMiddleware ...wish.Middleware) ssh.Option {
 44-	return func(server *ssh.Server) error {
 45-		err := sftp.SSHOption(handler)(server)
 46-		if err != nil {
 47-			return err
 48-		}
 49-
 50-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 51-
 52-		for name, subsystemHandler := range server.SubsystemHandlers {
 53-			newSubsystemHandlers[name] = func(s ssh.Session) {
 54-				wsh.LogMiddleware(handler.GetLogger(s), handler.Cfg.DB)(ssh.Handler(subsystemHandler))(s)
 55-			}
 56-		}
 57-
 58-		server.SubsystemHandlers = newSubsystemHandlers
 59-
 60-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 61-	}
 62-}
 63-
 64-func StartSshServer(cfg *PgsConfig, killCh chan error) {
 65-	host := utils.GetEnv("PGS_HOST", "0.0.0.0")
 66-	port := utils.GetEnv("PGS_SSH_PORT", "2222")
 67-	promPort := utils.GetEnv("PGS_PROM_PORT", "9222")
 68-	logger := cfg.Logger
 69-
 70-	ctx := context.Background()
 71-	defer ctx.Done()
 72-
 73-	cacheClearingQueue := make(chan string, 100)
 74-	handler := NewUploadAssetHandler(
 75-		cfg,
 76-		cacheClearingQueue,
 77-		ctx,
 78-	)
 79-
 80-	webTunnel := &tunkit.WebTunnelHandler{
 81-		Logger:      logger,
 82-		HttpHandler: createHttpHandler(cfg),
 83-	}
 84-
 85-	sshAuth := shared.NewSshAuthHandler(cfg.DB, logger)
 86-	s, err := wish.NewServer(
 87-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 88-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 89-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
 90-		tunkit.WithWebTunnel(webTunnel),
 91-		withProxy(
 92-			handler,
 93-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pgs-ssh"),
 94-		),
 95-	)
 96-	if err != nil {
 97-		logger.Error(err.Error())
 98-		return
 99-	}
100-
101-	done := make(chan os.Signal, 1)
102-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
103-	logger.Info("starting SSH server on", "host", host, "port", port)
104-
105-	go func() {
106-		if err = s.ListenAndServe(); err != nil {
107-			if err != ssh.ErrServerClosed {
108-				logger.Error("serve", "err", err.Error())
109-				os.Exit(1)
110-			}
111-		}
112-	}()
113-
114-	exit := func() {
115-		logger.Info("stopping ssh server")
116-		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
117-		defer func() { cancel() }()
118-		if err := s.Shutdown(ctx); err != nil {
119-			logger.Error("shutdown", "err", err.Error())
120-			os.Exit(1)
121-		}
122-	}
123-
124-	select {
125-	case <-killCh:
126-		exit()
127-	case <-done:
128-		exit()
129-	}
130-}
D pico/ssh.go
+0, -132
  1@@ -1,132 +0,0 @@
  2-package pico
  3-
  4-import (
  5-	"context"
  6-	"fmt"
  7-	"os"
  8-	"os/signal"
  9-	"syscall"
 10-	"time"
 11-
 12-	"git.sr.ht/~rockorager/vaxis"
 13-	"github.com/charmbracelet/promwish"
 14-	"github.com/charmbracelet/ssh"
 15-	"github.com/charmbracelet/wish"
 16-	"github.com/picosh/pico/db/postgres"
 17-	"github.com/picosh/pico/shared"
 18-	"github.com/picosh/pico/tui"
 19-	wsh "github.com/picosh/pico/wish"
 20-	"github.com/picosh/send/auth"
 21-	"github.com/picosh/send/list"
 22-	"github.com/picosh/send/pipe"
 23-	wishrsync "github.com/picosh/send/protocols/rsync"
 24-	"github.com/picosh/send/protocols/scp"
 25-	"github.com/picosh/send/protocols/sftp"
 26-	"github.com/picosh/send/proxy"
 27-	"github.com/picosh/utils"
 28-)
 29-
 30-func createRouterVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler) proxy.Router {
 31-	return func(sh ssh.Handler, sesh ssh.Session) []wish.Middleware {
 32-		shrd := &tui.SharedModel{
 33-			Session: sesh,
 34-			Cfg:     cfg,
 35-			Dbpool:  handler.DBPool,
 36-			Logger:  cfg.Logger,
 37-		}
 38-		return []wish.Middleware{
 39-			pipe.Middleware(handler, ""),
 40-			list.Middleware(handler),
 41-			scp.Middleware(handler),
 42-			wishrsync.Middleware(handler),
 43-			auth.Middleware(handler),
 44-			wsh.PtyMdw(createTui(shrd)),
 45-			WishMiddleware(cliHandler),
 46-			wsh.LogMiddleware(handler.GetLogger(sesh), handler.DBPool),
 47-		}
 48-	}
 49-}
 50-
 51-func withProxyVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler, otherMiddleware ...wish.Middleware) ssh.Option {
 52-	return func(server *ssh.Server) error {
 53-		err := sftp.SSHOption(handler)(server)
 54-		if err != nil {
 55-			return err
 56-		}
 57-
 58-		return proxy.WithProxy(createRouterVaxis(cfg, handler, cliHandler), otherMiddleware...)(server)
 59-	}
 60-}
 61-
 62-func createTui(shrd *tui.SharedModel) wish.Middleware {
 63-	return func(next ssh.Handler) ssh.Handler {
 64-		return func(sesh ssh.Session) {
 65-			vty, err := shared.NewVConsole(sesh)
 66-			if err != nil {
 67-				panic(err)
 68-			}
 69-			opts := vaxis.Options{
 70-				WithConsole: vty,
 71-			}
 72-			tui.NewTui(opts, shrd)
 73-		}
 74-	}
 75-}
 76-
 77-func StartSshServer() {
 78-	host := utils.GetEnv("PICO_HOST", "0.0.0.0")
 79-	port := utils.GetEnv("PICO_SSH_PORT", "2222")
 80-	promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
 81-	cfg := NewConfigSite("pico-ssh")
 82-	logger := cfg.Logger
 83-	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
 84-	defer dbpool.Close()
 85-
 86-	handler := NewUploadHandler(
 87-		dbpool,
 88-		cfg,
 89-	)
 90-	cliHandler := &CliHandler{
 91-		Logger: logger,
 92-		DBPool: dbpool,
 93-	}
 94-
 95-	sshAuth := shared.NewSshAuthHandler(dbpool, logger)
 96-	s, err := wish.NewServer(
 97-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 98-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 99-		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
100-			sshAuth.PubkeyAuthHandler(ctx, key)
101-			return true
102-		}),
103-		withProxyVaxis(
104-			cfg,
105-			handler,
106-			cliHandler,
107-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pico-ssh"),
108-		),
109-	)
110-	if err != nil {
111-		logger.Error(err.Error())
112-		return
113-	}
114-
115-	done := make(chan os.Signal, 1)
116-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
117-	logger.Info("starting SSH server on", "host", host, "port", port)
118-	go func() {
119-		if err = s.ListenAndServe(); err != nil {
120-			logger.Error("serve", "err", err.Error())
121-			os.Exit(1)
122-		}
123-	}()
124-
125-	<-done
126-	logger.Info("stopping SSH server")
127-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
128-	defer func() { cancel() }()
129-	if err := s.Shutdown(ctx); err != nil {
130-		logger.Error("shutdown", "err", err.Error())
131-		os.Exit(1)
132-	}
133-}
D pipe/ssh.go
+0, -80
 1@@ -1,80 +0,0 @@
 2-package pipe
 3-
 4-import (
 5-	"context"
 6-	"fmt"
 7-	"os"
 8-	"os/signal"
 9-	"syscall"
10-	"time"
11-
12-	"github.com/antoniomika/syncmap"
13-	"github.com/charmbracelet/promwish"
14-	"github.com/charmbracelet/ssh"
15-	"github.com/charmbracelet/wish"
16-	"github.com/picosh/pico/db/postgres"
17-	"github.com/picosh/pico/shared"
18-	wsh "github.com/picosh/pico/wish"
19-	psub "github.com/picosh/pubsub"
20-	"github.com/picosh/utils"
21-)
22-
23-func StartSshServer() {
24-	host := utils.GetEnv("PIPE_HOST", "0.0.0.0")
25-	port := utils.GetEnv("PIPE_SSH_PORT", "2222")
26-	portOverride := utils.GetEnv("PIPE_SSH_PORT_OVERRIDE", port)
27-	promPort := utils.GetEnv("PIPE_PROM_PORT", "9222")
28-	cfg := NewConfigSite("pipe-ssh")
29-	logger := cfg.Logger
30-	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
31-	defer dbh.Close()
32-
33-	cfg.Port = port
34-	cfg.PortOverride = portOverride
35-
36-	pubsub := psub.NewMulticast(logger)
37-	handler := &CliHandler{
38-		Logger:  logger,
39-		DBPool:  dbh,
40-		PubSub:  pubsub,
41-		Cfg:     cfg,
42-		Waiters: syncmap.New[string, []string](),
43-		Access:  syncmap.New[string, []string](),
44-	}
45-
46-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
47-	s, err := wish.NewServer(
48-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
49-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
50-		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
51-			sshAuth.PubkeyAuthHandler(ctx, key)
52-			return true
53-		}),
54-		wish.WithMiddleware(
55-			WishMiddleware(handler),
56-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pipe-ssh"),
57-			wsh.LogMiddleware(logger, dbh),
58-		),
59-	)
60-	if err != nil {
61-		logger.Error("wish server", "err", err.Error())
62-		return
63-	}
64-
65-	done := make(chan os.Signal, 1)
66-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
67-	logger.Info("Starting SSH server", "host", host, "port", port)
68-	go func() {
69-		if err = s.ListenAndServe(); err != nil {
70-			logger.Error("listen", "err", err.Error())
71-		}
72-	}()
73-
74-	<-done
75-	logger.Info("Stopping SSH server")
76-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
77-	defer func() { cancel() }()
78-	if err := s.Shutdown(ctx); err != nil {
79-		logger.Error("shutdown", "err", err.Error())
80-	}
81-}
R auth/__snapshots__/api_test.snap => pkg/apps/auth/__snapshots__/api_test.snap
+0, -0
R auth/api.go => pkg/apps/auth/api.go
+3, -3
 1@@ -16,9 +16,9 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/db/postgres"
10+	"github.com/picosh/pico/pkg/shared"
11 	"github.com/picosh/utils"
12 	"github.com/picosh/utils/pipe/metrics"
13 	"github.com/prometheus/client_golang/prometheus/promhttp"
R auth/api_test.go => pkg/apps/auth/api_test.go
+3, -3
 1@@ -12,9 +12,9 @@ import (
 2 	"time"
 3 
 4 	"github.com/gkampitakis/go-snaps/snaps"
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/stub"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/db/stub"
10+	"github.com/picosh/pico/pkg/shared"
11 )
12 
13 var testUserID = "user-1"
R auth/html/base.layout.tmpl => pkg/apps/auth/html/base.layout.tmpl
+0, -0
R auth/html/footer.partial.tmpl => pkg/apps/auth/html/footer.partial.tmpl
+0, -0
R auth/html/marketing-footer.partial.tmpl => pkg/apps/auth/html/marketing-footer.partial.tmpl
+0, -0
R auth/html/redirect.page.tmpl => pkg/apps/auth/html/redirect.page.tmpl
+0, -0
R auth/public/apple-touch-icon.png => pkg/apps/auth/public/apple-touch-icon.png
+0, -0
R auth/public/favicon-16x16.png => pkg/apps/auth/public/favicon-16x16.png
+0, -0
R auth/public/favicon.ico => pkg/apps/auth/public/favicon.ico
+0, -0
R auth/public/main.css => pkg/apps/auth/public/main.css
+0, -0
R auth/public/robots.txt => pkg/apps/auth/public/robots.txt
+0, -0
R feeds/api.go => pkg/apps/feeds/api.go
+2, -2
 1@@ -6,8 +6,8 @@ import (
 2 	"net/url"
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db/postgres"
 8+	"github.com/picosh/pico/pkg/shared"
 9 	"github.com/prometheus/client_golang/prometheus/promhttp"
10 )
11 
R feeds/cli.go => pkg/apps/feeds/cli.go
+34, -38
  1@@ -5,34 +5,31 @@ import (
  2 	"text/tabwriter"
  3 	"time"
  4 
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7-	"github.com/picosh/pico/db"
  8-	"github.com/picosh/pico/shared"
  9-
 10-	wsh "github.com/picosh/pico/wish"
 11+	"github.com/picosh/pico/pkg/db"
 12+	"github.com/picosh/pico/pkg/pssh"
 13+	"github.com/picosh/pico/pkg/shared"
 14 )
 15 
 16-func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) wish.Middleware {
 17-	return func(next ssh.Handler) ssh.Handler {
 18-		return func(sesh ssh.Session) {
 19+func Middleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddleware {
 20+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 21+		return func(sesh *pssh.SSHServerConnSession) error {
 22 			args := sesh.Command()
 23 			if len(args) == 0 {
 24-				next(sesh)
 25-				return
 26+				return next(sesh)
 27 			}
 28 
 29-			logger := wsh.GetLogger(sesh)
 30-			user := wsh.GetUser(sesh)
 31+			logger := pssh.GetLogger(sesh)
 32+			user := pssh.GetUser(sesh)
 33 
 34 			if user == nil {
 35-				wish.Errorln(sesh, fmt.Errorf("user not found"))
 36-				return
 37+				err := fmt.Errorf("user not found")
 38+				fmt.Fprintln(sesh.Stderr(), err)
 39+				return err
 40 			}
 41 
 42 			cmd := args[0]
 43 			if cmd == "help" {
 44-				wish.Printf(sesh, "Commands: [help, ls, rm, run]\n\n")
 45+				fmt.Fprintf(sesh, "Commands: [help, ls, rm, run]\n\n")
 46 				writer := tabwriter.NewWriter(sesh, 0, 0, 1, ' ', tabwriter.TabIndent)
 47 				fmt.Fprintln(writer, "Cmd\tDesc")
 48 				fmt.Fprintf(
 49@@ -55,17 +52,16 @@ func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) wish.Middleware {
 50 					"%s\t%s\n",
 51 					"run {filename}", "runs the feed digest post immediately, ignoring last digest time validation",
 52 				)
 53-				writer.Flush()
 54-				return
 55+				return writer.Flush()
 56 			} else if cmd == "ls" {
 57 				posts, err := dbpool.FindPostsForUser(&db.Pager{Page: 0, Num: 1000}, user.ID, "feeds")
 58 				if err != nil {
 59-					wish.Errorln(sesh, err)
 60-					return
 61+					fmt.Fprintln(sesh.Stderr(), err)
 62+					return err
 63 				}
 64 
 65 				if len(posts.Data) == 0 {
 66-					wish.Println(sesh, "no posts found")
 67+					fmt.Fprintln(sesh, "no posts found")
 68 				}
 69 
 70 				writer := tabwriter.NewWriter(sesh, 0, 0, 1, ' ', tabwriter.TabIndent)
 71@@ -83,11 +79,10 @@ func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) wish.Middleware {
 72 						post.Data.Attempts,
 73 					)
 74 				}
 75-				writer.Flush()
 76-				return
 77+				return writer.Flush()
 78 			} else if cmd == "rm" {
 79 				filename := args[1]
 80-				wish.Printf(sesh, "removing digest post %s\n", filename)
 81+				fmt.Fprintf(sesh, "removing digest post %s\n", filename)
 82 				write := false
 83 				if len(args) > 2 {
 84 					writeRaw := args[2]
 85@@ -98,41 +93,42 @@ func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) wish.Middleware {
 86 
 87 				post, err := dbpool.FindPostWithFilename(filename, user.ID, "feeds")
 88 				if err != nil {
 89-					wish.Errorln(sesh, err)
 90-					return
 91+					fmt.Fprintln(sesh.Stderr(), err)
 92+					return err
 93 				}
 94 				if write {
 95 					err = dbpool.RemovePosts([]string{post.ID})
 96 					if err != nil {
 97-						wish.Errorln(sesh, err)
 98+						fmt.Fprintln(sesh.Stderr(), err)
 99 					}
100 				}
101-				wish.Printf(sesh, "digest post removed %s\n", filename)
102+				fmt.Fprintf(sesh, "digest post removed %s\n", filename)
103 				if !write {
104-					wish.Println(sesh, "WARNING: *must* append with `--write` for the changes to persist.")
105+					fmt.Fprintln(sesh, "WARNING: *must* append with `--write` for the changes to persist.")
106 				}
107-				return
108+				return err
109 			} else if cmd == "run" {
110 				if len(args) < 2 {
111-					wish.Errorln(sesh, "must provide filename of post to run")
112-					return
113+					err := fmt.Errorf("must provide filename of post to run")
114+					fmt.Fprintln(sesh.Stderr(), err)
115+					return err
116 				}
117 				filename := args[1]
118 				post, err := dbpool.FindPostWithFilename(filename, user.ID, "feeds")
119 				if err != nil {
120-					wish.Errorln(sesh, err)
121-					return
122+					fmt.Fprintln(sesh.Stderr(), err)
123+					return err
124 				}
125-				wish.Printf(sesh, "running feed post: %s\n", filename)
126+				fmt.Fprintf(sesh, "running feed post: %s\n", filename)
127 				fetcher := NewFetcher(dbpool, cfg)
128 				err = fetcher.RunPost(logger, user, post, true)
129 				if err != nil {
130-					wish.Errorln(sesh, err)
131+					fmt.Fprintln(sesh.Stderr(), err)
132 				}
133-				return
134+				return err
135 			}
136 
137-			next(sesh)
138+			return next(sesh)
139 		}
140 	}
141 }
R feeds/config.go => pkg/apps/feeds/config.go
+1, -1
1@@ -1,7 +1,7 @@
2 package feeds
3 
4 import (
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils"
8 )
9 
R feeds/cron.go => pkg/apps/feeds/cron.go
+2, -2
 1@@ -15,8 +15,8 @@ import (
 2 	"time"
 3 
 4 	"github.com/mmcdole/gofeed"
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db"
 8+	"github.com/picosh/pico/pkg/shared"
 9 	"github.com/sendgrid/sendgrid-go"
10 	"github.com/sendgrid/sendgrid-go/helpers/mail"
11 )
R feeds/html/base.layout.tmpl => pkg/apps/feeds/html/base.layout.tmpl
+0, -0
R feeds/html/digest.page.tmpl => pkg/apps/feeds/html/digest.page.tmpl
+0, -0
R feeds/html/digest_text.page.tmpl => pkg/apps/feeds/html/digest_text.page.tmpl
+0, -0
R feeds/html/footer.partial.tmpl => pkg/apps/feeds/html/footer.partial.tmpl
+0, -0
R feeds/html/marketing-footer.partial.tmpl => pkg/apps/feeds/html/marketing-footer.partial.tmpl
+0, -0
R feeds/html/marketing.page.tmpl => pkg/apps/feeds/html/marketing.page.tmpl
+0, -0
R feeds/public/apple-touch-icon.png => pkg/apps/feeds/public/apple-touch-icon.png
+0, -0
R feeds/public/card.png => pkg/apps/feeds/public/card.png
+0, -0
R feeds/public/favicon-16x16.png => pkg/apps/feeds/public/favicon-16x16.png
+0, -0
R feeds/public/favicon.ico => pkg/apps/feeds/public/favicon.ico
+0, -0
R feeds/public/main.css => pkg/apps/feeds/public/main.css
+0, -0
R feeds/public/robots.txt => pkg/apps/feeds/public/robots.txt
+0, -0
R feeds/scp_hooks.go => pkg/apps/feeds/scp_hooks.go
+6, -6
 1@@ -8,10 +8,10 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/filehandlers"
 8-	"github.com/picosh/pico/shared"
 9+	"github.com/picosh/pico/pkg/db"
10+	"github.com/picosh/pico/pkg/filehandlers"
11+	"github.com/picosh/pico/pkg/pssh"
12+	"github.com/picosh/pico/pkg/shared"
13 	"github.com/picosh/utils"
14 )
15 
16@@ -20,7 +20,7 @@ type FeedHooks struct {
17 	Db  db.DB
18 }
19 
20-func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
21+func (p *FeedHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
22 	if !utils.IsTextFile(string(data.Text)) {
23 		err := fmt.Errorf(
24 			"WARNING: (%s) invalid file must be plain text (utf-8), skipping",
25@@ -73,7 +73,7 @@ func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
26 	return true, nil
27 }
28 
29-func (p *FeedHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
30+func (p *FeedHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
31 	if data.Data.LastDigest == nil {
32 		now := time.Now()
33 		// let it run on the next loop
A pkg/apps/feeds/ssh.go
+97, -0
 1@@ -0,0 +1,97 @@
 2+package feeds
 3+
 4+import (
 5+	"context"
 6+	"os"
 7+	"os/signal"
 8+	"syscall"
 9+
10+	"github.com/picosh/pico/pkg/db/postgres"
11+	"github.com/picosh/pico/pkg/filehandlers"
12+	"github.com/picosh/pico/pkg/pssh"
13+	"github.com/picosh/pico/pkg/send/auth"
14+	"github.com/picosh/pico/pkg/send/list"
15+	"github.com/picosh/pico/pkg/send/pipe"
16+	"github.com/picosh/pico/pkg/send/protocols/rsync"
17+	"github.com/picosh/pico/pkg/send/protocols/scp"
18+	"github.com/picosh/pico/pkg/send/protocols/sftp"
19+	"github.com/picosh/pico/pkg/shared"
20+	"github.com/picosh/utils"
21+)
22+
23+func StartSshServer() {
24+	appName := "feeds-ssh"
25+
26+	host := utils.GetEnv("FEEDS_HOST", "0.0.0.0")
27+	port := utils.GetEnv("FEEDS_SSH_PORT", "2222")
28+	promPort := utils.GetEnv("FEEDS_PROM_PORT", "9222")
29+	cfg := NewConfigSite(appName)
30+	logger := cfg.Logger
31+
32+	ctx, cancel := context.WithCancel(context.Background())
33+	defer cancel()
34+
35+	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
36+	defer dbh.Close()
37+
38+	hooks := &FeedHooks{
39+		Cfg: cfg,
40+		Db:  dbh,
41+	}
42+
43+	fileMap := map[string]filehandlers.ReadWriteHandler{
44+		"fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
45+	}
46+	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
47+
48+	sshAuth := shared.NewSshAuthHandler(dbh, logger)
49+
50+	// Create a new SSH server
51+	server, err := pssh.NewSSHServerWithConfig(
52+		ctx,
53+		logger,
54+		appName,
55+		host,
56+		port,
57+		promPort,
58+		sshAuth.PubkeyAuthHandler,
59+		[]pssh.SSHServerMiddleware{
60+			pipe.Middleware(handler, ".txt"),
61+			list.Middleware(handler),
62+			scp.Middleware(handler),
63+			rsync.Middleware(handler),
64+			auth.Middleware(handler),
65+			pssh.PtyMdw(pssh.DeprecatedNotice()),
66+			pssh.LogMiddleware(handler, dbh),
67+		},
68+		[]pssh.SSHServerMiddleware{
69+			sftp.Middleware(handler),
70+			pssh.LogMiddleware(handler, dbh),
71+		},
72+		nil,
73+	)
74+
75+	if err != nil {
76+		logger.Error("failed to create ssh server", "err", err.Error())
77+		os.Exit(1)
78+	}
79+
80+	done := make(chan os.Signal, 1)
81+
82+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
83+	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
84+	go func() {
85+		if err = server.ListenAndServe(); err != nil {
86+			logger.Error("serve", "err", err.Error())
87+			os.Exit(1)
88+		}
89+	}()
90+
91+	exit := func() {
92+		logger.Info("stopping ssh server")
93+		cancel()
94+	}
95+
96+	<-done
97+	exit()
98+}
R pastes/api.go => pkg/apps/pastes/api.go
+3, -3
 1@@ -8,9 +8,9 @@ import (
 2 	"os"
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/db/postgres"
10+	"github.com/picosh/pico/pkg/shared"
11 	"github.com/picosh/utils"
12 	"github.com/prometheus/client_golang/prometheus/promhttp"
13 )
R pastes/config.go => pkg/apps/pastes/config.go
+1, -1
1@@ -1,7 +1,7 @@
2 package pastes
3 
4 import (
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils"
8 )
9 
R pastes/cron.go => pkg/apps/pastes/cron.go
+2, -2
 1@@ -3,8 +3,8 @@ package pastes
 2 import (
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db"
 8+	"github.com/picosh/pico/pkg/shared"
 9 )
10 
11 func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) error {
R pastes/html/base.layout.tmpl => pkg/apps/pastes/html/base.layout.tmpl
+0, -0
R pastes/html/blog.page.tmpl => pkg/apps/pastes/html/blog.page.tmpl
+0, -0
R pastes/html/footer.partial.tmpl => pkg/apps/pastes/html/footer.partial.tmpl
+0, -0
R pastes/html/marketing-footer.partial.tmpl => pkg/apps/pastes/html/marketing-footer.partial.tmpl
+0, -0
R pastes/html/marketing.page.tmpl => pkg/apps/pastes/html/marketing.page.tmpl
+0, -0
R pastes/html/post.page.tmpl => pkg/apps/pastes/html/post.page.tmpl
+0, -0
R pastes/parser.go => pkg/apps/pastes/parser.go
+0, -0
R pastes/public/apple-touch-icon.png => pkg/apps/pastes/public/apple-touch-icon.png
+0, -0
R pastes/public/card.png => pkg/apps/pastes/public/card.png
+0, -0
R pastes/public/favicon-16x16.png => pkg/apps/pastes/public/favicon-16x16.png
+0, -0
R pastes/public/favicon.ico => pkg/apps/pastes/public/favicon.ico
+0, -0
R pastes/public/main.css => pkg/apps/pastes/public/main.css
+0, -0
R pastes/public/robots.txt => pkg/apps/pastes/public/robots.txt
+0, -0
R pastes/public/smol.css => pkg/apps/pastes/public/smol.css
+0, -0
R pastes/public/syntax.css => pkg/apps/pastes/public/syntax.css
+0, -0
R pastes/scp_hooks.go => pkg/apps/pastes/scp_hooks.go
+6, -6
 1@@ -7,10 +7,10 @@ import (
 2 	"time"
 3 
 4 	"github.com/araddon/dateparse"
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/filehandlers"
 8-	"github.com/picosh/pico/shared"
 9+	"github.com/picosh/pico/pkg/db"
10+	"github.com/picosh/pico/pkg/filehandlers"
11+	"github.com/picosh/pico/pkg/pssh"
12+	"github.com/picosh/pico/pkg/shared"
13 	"github.com/picosh/utils"
14 )
15 
16@@ -21,7 +21,7 @@ type FileHooks struct {
17 	Db  db.DB
18 }
19 
20-func (p *FileHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
21+func (p *FileHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
22 	if !utils.IsTextFile(string(data.Text)) {
23 		err := fmt.Errorf(
24 			"ERROR: (%s) invalid file must be plain text (utf-8), skipping",
25@@ -42,7 +42,7 @@ func (p *FileHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
26 	return true, nil
27 }
28 
29-func (p *FileHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
30+func (p *FileHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
31 	data.Title = utils.ToUpper(data.Slug)
32 	// we want the slug to be the filename for pastes
33 	data.Slug = data.Filename
A pkg/apps/pastes/ssh.go
+94, -0
 1@@ -0,0 +1,94 @@
 2+package pastes
 3+
 4+import (
 5+	"context"
 6+	"os"
 7+	"os/signal"
 8+	"syscall"
 9+
10+	"github.com/picosh/pico/pkg/db/postgres"
11+	"github.com/picosh/pico/pkg/filehandlers"
12+	"github.com/picosh/pico/pkg/pssh"
13+	"github.com/picosh/pico/pkg/send/auth"
14+	"github.com/picosh/pico/pkg/send/list"
15+	"github.com/picosh/pico/pkg/send/pipe"
16+	"github.com/picosh/pico/pkg/send/protocols/rsync"
17+	"github.com/picosh/pico/pkg/send/protocols/scp"
18+	"github.com/picosh/pico/pkg/send/protocols/sftp"
19+	"github.com/picosh/pico/pkg/shared"
20+	"github.com/picosh/utils"
21+)
22+
23+func StartSshServer() {
24+	appName := "pastes-ssh"
25+
26+	host := utils.GetEnv("PASTES_HOST", "0.0.0.0")
27+	port := utils.GetEnv("PASTES_SSH_PORT", "2222")
28+	promPort := utils.GetEnv("PASTES_PROM_PORT", "9222")
29+	cfg := NewConfigSite(appName)
30+	logger := cfg.Logger
31+
32+	ctx, cancel := context.WithCancel(context.Background())
33+	defer cancel()
34+
35+	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
36+	defer dbh.Close()
37+	hooks := &FileHooks{
38+		Cfg: cfg,
39+		Db:  dbh,
40+	}
41+
42+	fileMap := map[string]filehandlers.ReadWriteHandler{
43+		"fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
44+	}
45+	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
46+	sshAuth := shared.NewSshAuthHandler(dbh, logger)
47+
48+	// Create a new SSH server
49+	server, err := pssh.NewSSHServerWithConfig(
50+		ctx,
51+		logger,
52+		appName,
53+		host,
54+		port,
55+		promPort,
56+		sshAuth.PubkeyAuthHandler,
57+		[]pssh.SSHServerMiddleware{
58+			pipe.Middleware(handler, ""),
59+			list.Middleware(handler),
60+			scp.Middleware(handler),
61+			rsync.Middleware(handler),
62+			auth.Middleware(handler),
63+			pssh.PtyMdw(pssh.DeprecatedNotice()),
64+			pssh.LogMiddleware(handler, dbh),
65+		},
66+		[]pssh.SSHServerMiddleware{
67+			sftp.Middleware(handler),
68+			pssh.LogMiddleware(handler, dbh),
69+		},
70+		nil,
71+	)
72+
73+	if err != nil {
74+		logger.Error("failed to create ssh server", "err", err.Error())
75+		os.Exit(1)
76+	}
77+
78+	done := make(chan os.Signal, 1)
79+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
80+	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
81+	go func() {
82+		if err = server.ListenAndServe(); err != nil {
83+			logger.Error("serve", "err", err.Error())
84+			os.Exit(1)
85+		}
86+	}()
87+
88+	exit := func() {
89+		logger.Info("stopping ssh server")
90+		cancel()
91+	}
92+
93+	<-done
94+	exit()
95+}
R pgs/access.go => pkg/apps/pgs/access.go
+1, -1
1@@ -3,7 +3,7 @@ package pgs
2 import (
3 	"slices"
4 
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"golang.org/x/crypto/ssh"
8 )
9 
R pgs/calc_route.go => pkg/apps/pgs/calc_route.go
+3, -3
 1@@ -8,9 +8,9 @@ import (
 2 	"regexp"
 3 	"strings"
 4 
 5-	"github.com/picosh/pico/shared"
 6-	"github.com/picosh/pico/shared/storage"
 7-	"github.com/picosh/send/utils"
 8+	"github.com/picosh/pico/pkg/send/utils"
 9+	"github.com/picosh/pico/pkg/shared"
10+	"github.com/picosh/pico/pkg/shared/storage"
11 )
12 
13 type HttpReply struct {
R pgs/calc_route_test.go => pkg/apps/pgs/calc_route_test.go
+0, -0
R pgs/cli.go => pkg/apps/pgs/cli.go
+97, -117
  1@@ -4,120 +4,45 @@ import (
  2 	"context"
  3 	"errors"
  4 	"fmt"
  5+	"io"
  6 	"log/slog"
  7 	"path/filepath"
  8 	"strings"
  9+	"text/tabwriter"
 10 	"time"
 11 
 12-	"github.com/charmbracelet/lipgloss"
 13-	"github.com/charmbracelet/lipgloss/table"
 14-	"github.com/picosh/pico/db"
 15-	pgsdb "github.com/picosh/pico/pgs/db"
 16-	"github.com/picosh/pico/shared"
 17-	sst "github.com/picosh/pobj/storage"
 18+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 19+	"github.com/picosh/pico/pkg/db"
 20+	sst "github.com/picosh/pico/pkg/pobj/storage"
 21+	"github.com/picosh/pico/pkg/shared"
 22 	"github.com/picosh/utils"
 23 )
 24 
 25-func projectTable(projects []*db.Project, width int) *table.Table {
 26-	headers := []string{
 27-		"Name",
 28-		"Last Updated",
 29-		"Links To",
 30-		"ACL Type",
 31-		"ACL",
 32-		"Blocked",
 33-	}
 34-	data := [][]string{}
 35+func NewTabWriter(out io.Writer) *tabwriter.Writer {
 36+	return tabwriter.NewWriter(out, 0, 0, 1, ' ', tabwriter.TabIndent)
 37+}
 38+
 39+func projectTable(sesh io.Writer, projects []*db.Project) {
 40+	writer := NewTabWriter(sesh)
 41+	fmt.Fprintln(writer, "Name\tLast Updated\tLinks To\tACL Type\tACL\tBlocked")
 42+
 43 	for _, project := range projects {
 44-		row := []string{
 45-			project.Name,
 46-			project.UpdatedAt.Format("2006-01-02 15:04:05"),
 47-		}
 48 		links := ""
 49 		if project.ProjectDir != project.Name {
 50 			links = project.ProjectDir
 51 		}
 52-		row = append(row, links)
 53-		row = append(row,
 54+		fmt.Fprintf(
 55+			writer,
 56+			"%s\t%s\t%s\t%s\t%s\t%s\n",
 57+			project.Name,
 58+			project.UpdatedAt.Format("2006-01-02 15:04:05"),
 59+			links,
 60 			project.Acl.Type,
 61 			strings.Join(project.Acl.Data, " "),
 62+			project.Blocked,
 63 		)
 64-		row = append(row, project.Blocked)
 65-		data = append(data, row)
 66-	}
 67-
 68-	t := table.New().
 69-		Width(width).
 70-		Headers(headers...).
 71-		Rows(data...)
 72-	return t
 73-}
 74-
 75-func getHelpText(width int) string {
 76-	helpStr := "Commands: [help, stats, ls, fzf, rm, link, unlink, prune, retain, depends, acl, cache]\n"
 77-	helpStr += "NOTICE:" + " *must* append with `--write` for the changes to persist.\n"
 78-
 79-	projectName := "projA"
 80-	headers := []string{"Cmd", "Description"}
 81-	data := [][]string{
 82-		{
 83-			"help",
 84-			"prints this screen",
 85-		},
 86-		{
 87-			"stats",
 88-			"usage statistics",
 89-		},
 90-		{
 91-			"ls",
 92-			"lists projects",
 93-		},
 94-		{
 95-			fmt.Sprintf("fzf %s", projectName),
 96-			fmt.Sprintf("lists urls of all assets in %s", projectName),
 97-		},
 98-		{
 99-			fmt.Sprintf("rm %s", projectName),
100-			fmt.Sprintf("delete %s", projectName),
101-		},
102-		{
103-			fmt.Sprintf("link %s --to projB", projectName),
104-			fmt.Sprintf("symbolic link `%s` to `projB`", projectName),
105-		},
106-		{
107-			fmt.Sprintf("unlink %s", projectName),
108-			fmt.Sprintf("removes symbolic link for `%s`", projectName),
109-		},
110-		{
111-			fmt.Sprintf("prune %s", projectName),
112-			fmt.Sprintf("removes projects that match prefix `%s`", projectName),
113-		},
114-		{
115-			fmt.Sprintf("retain %s", projectName),
116-			"alias to `prune` but keeps last N projects",
117-		},
118-		{
119-			fmt.Sprintf("depends %s", projectName),
120-			fmt.Sprintf("lists all projects linked to `%s`", projectName),
121-		},
122-		{
123-			fmt.Sprintf("acl %s", projectName),
124-			fmt.Sprintf("access control for `%s`", projectName),
125-		},
126-		{
127-			fmt.Sprintf("cache %s", projectName),
128-			fmt.Sprintf("clear http cache for `%s`", projectName),
129-		},
130 	}
131-
132-	t := table.New().
133-		Width(width).
134-		Border(lipgloss.RoundedBorder()).
135-		Headers(headers...).
136-		Rows(data...)
137-
138-	helpStr += t.String()
139-	return helpStr
140+	writer.Flush()
141 }
142 
143 type Cmd struct {
144@@ -201,7 +126,68 @@ func (c *Cmd) RmProjectAssets(projectName string) error {
145 }
146 
147 func (c *Cmd) help() {
148-	c.output(getHelpText(c.Width))
149+	helpStr := "Commands: [help, stats, ls, fzf, rm, link, unlink, prune, retain, depends, acl, cache]\n"
150+	helpStr += "NOTICE:" + " *must* append with `--write` for the changes to persist.\n"
151+	c.output(helpStr)
152+	projectName := "projA"
153+
154+	data := [][]string{
155+		{
156+			"help",
157+			"prints this screen",
158+		},
159+		{
160+			"stats",
161+			"usage statistics",
162+		},
163+		{
164+			"ls",
165+			"lists projects",
166+		},
167+		{
168+			fmt.Sprintf("fzf %s", projectName),
169+			fmt.Sprintf("lists urls of all assets in %s", projectName),
170+		},
171+		{
172+			fmt.Sprintf("rm %s", projectName),
173+			fmt.Sprintf("delete %s", projectName),
174+		},
175+		{
176+			fmt.Sprintf("link %s --to projB", projectName),
177+			fmt.Sprintf("symbolic link `%s` to `projB`", projectName),
178+		},
179+		{
180+			fmt.Sprintf("unlink %s", projectName),
181+			fmt.Sprintf("removes symbolic link for `%s`", projectName),
182+		},
183+		{
184+			fmt.Sprintf("prune %s", projectName),
185+			fmt.Sprintf("removes projects that match prefix `%s`", projectName),
186+		},
187+		{
188+			fmt.Sprintf("retain %s", projectName),
189+			"alias to `prune` but keeps last N projects",
190+		},
191+		{
192+			fmt.Sprintf("depends %s", projectName),
193+			fmt.Sprintf("lists all projects linked to `%s`", projectName),
194+		},
195+		{
196+			fmt.Sprintf("acl %s", projectName),
197+			fmt.Sprintf("access control for `%s`", projectName),
198+		},
199+		{
200+			fmt.Sprintf("cache %s", projectName),
201+			fmt.Sprintf("clear http cache for `%s`", projectName),
202+		},
203+	}
204+
205+	writer := NewTabWriter(c.Session)
206+	fmt.Fprintln(writer, "Cmd\tDescription")
207+	for _, dat := range data {
208+		fmt.Fprintf(writer, "%s\t%s\n", dat[0], dat[1])
209+	}
210+	writer.Flush()
211 }
212 
213 func (c *Cmd) stats(cfgMaxSize uint64) error {
214@@ -229,20 +215,17 @@ func (c *Cmd) stats(cfgMaxSize uint64) error {
215 		return err
216 	}
217 
218-	headers := []string{"Used (GB)", "Quota (GB)", "Used (%)", "Projects (#)"}
219-	data := []string{
220-		fmt.Sprintf("%.4f", utils.BytesToGB(int(totalFileSize))),
221-		fmt.Sprintf("%.4f", utils.BytesToGB(int(storageMax))),
222-		fmt.Sprintf("%.4f", (float32(totalFileSize)/float32(storageMax))*100),
223-		fmt.Sprintf("%d", len(projects)),
224-	}
225-
226-	t := table.New().
227-		Width(c.Width).
228-		Border(lipgloss.RoundedBorder()).
229-		Headers(headers...).
230-		Rows(data)
231-	c.output(t.String())
232+	writer := NewTabWriter(c.Session)
233+	fmt.Fprintln(writer, "Used (GB)\tQuota (GB)\tUsed (%)\tProjects (#)")
234+	fmt.Fprintf(
235+		writer,
236+		"%.4f\t%.4f\t%.4f\t%d\n",
237+		utils.BytesToGB(int(totalFileSize)),
238+		utils.BytesToGB(int(storageMax)),
239+		(float32(totalFileSize)/float32(storageMax))*100,
240+		len(projects),
241+	)
242+	writer.Flush()
243 
244 	return nil
245 }
246@@ -257,8 +240,7 @@ func (c *Cmd) ls() error {
247 		c.output("no projects found")
248 	}
249 
250-	t := projectTable(projects, c.Width)
251-	c.output(t.String())
252+	projectTable(c.Session, projects)
253 
254 	return nil
255 }
256@@ -374,9 +356,7 @@ func (c *Cmd) depends(projectName string) error {
257 		return nil
258 	}
259 
260-	t := projectTable(projects, c.Width)
261-	c.output(t.String())
262-
263+	projectTable(c.Session, projects)
264 	return nil
265 }
266 
R pgs/cli_wish.go => pkg/apps/pgs/cli_wish.go
+42, -45
  1@@ -6,17 +6,14 @@ import (
  2 	"slices"
  3 	"strings"
  4 
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7-	bm "github.com/charmbracelet/wish/bubbletea"
  8-	"github.com/muesli/termenv"
  9-	"github.com/picosh/pico/db"
 10-	pgsdb "github.com/picosh/pico/pgs/db"
 11-	sendutils "github.com/picosh/send/utils"
 12+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 13+	"github.com/picosh/pico/pkg/db"
 14+	"github.com/picosh/pico/pkg/pssh"
 15+	sendutils "github.com/picosh/pico/pkg/send/utils"
 16 	"github.com/picosh/utils"
 17 )
 18 
 19-func getUser(s ssh.Session, dbpool pgsdb.PgsDB) (*db.User, error) {
 20+func getUser(s *pssh.SSHServerConnSession, dbpool pgsdb.PgsDB) (*db.User, error) {
 21 	if s.PublicKey() == nil {
 22 		return nil, fmt.Errorf("key not found")
 23 	}
 24@@ -46,7 +43,7 @@ func (i *arrayFlags) Set(value string) error {
 25 	return nil
 26 }
 27 
 28-func flagSet(cmdName string, sesh ssh.Session) (*flag.FlagSet, *bool) {
 29+func flagSet(cmdName string, sesh *pssh.SSHServerConnSession) (*flag.FlagSet, *bool) {
 30 	cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
 31 	cmd.SetOutput(sesh)
 32 	write := cmd.Bool("write", false, "apply changes")
 33@@ -63,18 +60,17 @@ func flagCheck(cmd *flag.FlagSet, posArg string, cmdArgs []string) bool {
 34 	return true
 35 }
 36 
 37-func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 38+func Middleware(handler *UploadAssetHandler) pssh.SSHServerMiddleware {
 39 	dbpool := handler.Cfg.DB
 40 	log := handler.Cfg.Logger
 41 	cfg := handler.Cfg
 42 	store := handler.Cfg.Storage
 43 
 44-	return func(next ssh.Handler) ssh.Handler {
 45-		return func(sesh ssh.Session) {
 46+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 47+		return func(sesh *pssh.SSHServerConnSession) error {
 48 			args := sesh.Command()
 49 			if len(args) == 0 {
 50-				next(sesh)
 51-				return
 52+				return next(sesh)
 53 			}
 54 
 55 			// default width and height when no pty
 56@@ -89,11 +85,12 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 57 			user, err := getUser(sesh, dbpool)
 58 			if err != nil {
 59 				sendutils.ErrorHandler(sesh, err)
 60-				return
 61+				return err
 62 			}
 63 
 64-			renderer := bm.MakeRenderer(sesh)
 65-			renderer.SetColorProfile(termenv.TrueColor)
 66+			// renderer := bm.MakeRenderer(sesh)
 67+			// renderer.SetColorProfile(termenv.TrueColor)
 68+			// styles := common.DefaultStyles(renderer)
 69 
 70 			opts := Cmd{
 71 				Session: sesh,
 72@@ -102,33 +99,33 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 73 				Log:     log,
 74 				Dbpool:  dbpool,
 75 				Write:   false,
 76-				Width:   width,
 77-				Height:  height,
 78-				Cfg:     handler.Cfg,
 79+				// Styles:  styles,
 80+				Width:  width,
 81+				Height: height,
 82+				Cfg:    handler.Cfg,
 83 			}
 84 
 85 			cmd := strings.TrimSpace(args[0])
 86 			if len(args) == 1 {
 87 				if cmd == "help" {
 88 					opts.help()
 89-					return
 90+					return nil
 91 				} else if cmd == "stats" {
 92 					err := opts.stats(cfg.MaxSize)
 93 					opts.bail(err)
 94-					return
 95+					return err
 96 				} else if cmd == "ls" {
 97 					err := opts.ls()
 98 					opts.bail(err)
 99-					return
100+					return err
101 				} else if cmd == "cache-all" {
102 					opts.Write = true
103 					err := opts.cacheAll()
104 					opts.notice()
105 					opts.bail(err)
106-					return
107+					return err
108 				} else {
109-					next(sesh)
110-					return
111+					return next(sesh)
112 				}
113 			}
114 
115@@ -145,12 +142,12 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
116 			if cmd == "fzf" {
117 				err := opts.fzf(projectName)
118 				opts.bail(err)
119-				return
120+				return err
121 			} else if cmd == "link" {
122 				linkCmd, write := flagSet("link", sesh)
123 				linkTo := linkCmd.String("to", "", "symbolic link to this project")
124 				if !flagCheck(linkCmd, projectName, cmdArgs) {
125-					return
126+					return nil
127 				}
128 				opts.Write = *write
129 
130@@ -159,7 +156,7 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
131 						"must provide `--to` flag",
132 					)
133 					opts.bail(err)
134-					return
135+					return err
136 				}
137 
138 				err := opts.link(projectName, *linkTo)
139@@ -167,67 +164,67 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
140 				if err != nil {
141 					opts.bail(err)
142 				}
143-				return
144+				return err
145 			} else if cmd == "unlink" {
146 				unlinkCmd, write := flagSet("unlink", sesh)
147 				if !flagCheck(unlinkCmd, projectName, cmdArgs) {
148-					return
149+					return nil
150 				}
151 				opts.Write = *write
152 
153 				err := opts.unlink(projectName)
154 				opts.notice()
155 				opts.bail(err)
156-				return
157+				return err
158 			} else if cmd == "depends" {
159 				err := opts.depends(projectName)
160 				opts.bail(err)
161-				return
162+				return err
163 			} else if cmd == "retain" {
164 				retainCmd, write := flagSet("retain", sesh)
165 				retainNum := retainCmd.Int("n", 3, "latest number of projects to keep")
166 				if !flagCheck(retainCmd, projectName, cmdArgs) {
167-					return
168+					return nil
169 				}
170 				opts.Write = *write
171 
172 				err := opts.prune(projectName, *retainNum)
173 				opts.notice()
174 				opts.bail(err)
175-				return
176+				return err
177 			} else if cmd == "prune" {
178 				pruneCmd, write := flagSet("prune", sesh)
179 				if !flagCheck(pruneCmd, projectName, cmdArgs) {
180-					return
181+					return nil
182 				}
183 				opts.Write = *write
184 
185 				err := opts.prune(projectName, 0)
186 				opts.notice()
187 				opts.bail(err)
188-				return
189+				return err
190 			} else if cmd == "rm" {
191 				rmCmd, write := flagSet("rm", sesh)
192 				if !flagCheck(rmCmd, projectName, cmdArgs) {
193-					return
194+					return nil
195 				}
196 				opts.Write = *write
197 
198 				err := opts.rm(projectName)
199 				opts.notice()
200 				opts.bail(err)
201-				return
202+				return err
203 			} else if cmd == "cache" {
204 				cacheCmd, write := flagSet("cache", sesh)
205 				if !flagCheck(cacheCmd, projectName, cmdArgs) {
206-					return
207+					return nil
208 				}
209 				opts.Write = *write
210 
211 				err := opts.cache(projectName)
212 				opts.notice()
213 				opts.bail(err)
214-				return
215+				return err
216 			} else if cmd == "acl" {
217 				aclCmd, write := flagSet("acl", sesh)
218 				aclType := aclCmd.String("type", "", "access type: public, pico, pubkeys")
219@@ -238,7 +235,7 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
220 					"list of pico usernames or sha256 public keys, delimited by commas",
221 				)
222 				if !flagCheck(aclCmd, projectName, cmdArgs) {
223-					return
224+					return nil
225 				}
226 				opts.Write = *write
227 
228@@ -248,15 +245,15 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
229 						*aclType,
230 					)
231 					opts.bail(err)
232-					return
233+					return err
234 				}
235 
236 				err := opts.acl(projectName, *aclType, acls)
237 				opts.notice()
238 				opts.bail(err)
239+				return err
240 			} else {
241-				next(sesh)
242-				return
243+				return next(sesh)
244 			}
245 		}
246 	}
R pgs/config.go => pkg/apps/pgs/config.go
+2, -2
 1@@ -6,8 +6,8 @@ import (
 2 	"path/filepath"
 3 	"time"
 4 
 5-	pgsdb "github.com/picosh/pico/pgs/db"
 6-	"github.com/picosh/pico/shared/storage"
 7+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 8+	"github.com/picosh/pico/pkg/shared/storage"
 9 	"github.com/picosh/utils"
10 )
11 
R pgs/db/db.go => pkg/apps/pgs/db/db.go
+1, -1
1@@ -1,6 +1,6 @@
2 package pgsdb
3 
4-import "github.com/picosh/pico/db"
5+import "github.com/picosh/pico/pkg/db"
6 
7 type PgsDB interface {
8 	FindUser(userID string) (*db.User, error)
R pgs/db/memory.go => pkg/apps/pgs/db/memory.go
+1, -1
1@@ -6,7 +6,7 @@ import (
2 	"time"
3 
4 	"github.com/google/uuid"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 )
9 
R pgs/db/postgres.go => pkg/apps/pgs/db/postgres.go
+1, -1
1@@ -7,7 +7,7 @@ import (
2 
3 	"github.com/jmoiron/sqlx"
4 	_ "github.com/lib/pq"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 )
9 
R pgs/fs.go => pkg/apps/pgs/fs.go
+0, -0
R pgs/header.go => pkg/apps/pgs/header.go
+0, -0
R pgs/header_test.go => pkg/apps/pgs/header_test.go
+0, -0
R pgs/html/base.layout.tmpl => pkg/apps/pgs/html/base.layout.tmpl
+0, -0
R pgs/html/footer.partial.tmpl => pkg/apps/pgs/html/footer.partial.tmpl
+0, -0
R pgs/html/marketing-footer.partial.tmpl => pkg/apps/pgs/html/marketing-footer.partial.tmpl
+0, -0
R pgs/html/marketing.page.tmpl => pkg/apps/pgs/html/marketing.page.tmpl
+0, -0
R pgs/public/card.png => pkg/apps/pgs/public/card.png
+0, -0
R pgs/public/favicon-16x16.png => pkg/apps/pgs/public/favicon-16x16.png
+0, -0
R pgs/public/favicon.ico => pkg/apps/pgs/public/favicon.ico
+0, -0
R pgs/public/main.css => pkg/apps/pgs/public/main.css
+0, -0
R pgs/public/robots.txt => pkg/apps/pgs/public/robots.txt
+0, -0
R pgs/redirect.go => pkg/apps/pgs/redirect.go
+0, -0
R pgs/redirect_test.go => pkg/apps/pgs/redirect_test.go
+0, -0
A pkg/apps/pgs/ssh.go
+98, -0
 1@@ -0,0 +1,98 @@
 2+package pgs
 3+
 4+import (
 5+	"context"
 6+	"os"
 7+	"os/signal"
 8+	"syscall"
 9+
10+	"github.com/picosh/pico/pkg/pssh"
11+	"github.com/picosh/pico/pkg/send/auth"
12+	"github.com/picosh/pico/pkg/send/list"
13+	"github.com/picosh/pico/pkg/send/pipe"
14+	"github.com/picosh/pico/pkg/send/protocols/rsync"
15+	"github.com/picosh/pico/pkg/send/protocols/scp"
16+	"github.com/picosh/pico/pkg/send/protocols/sftp"
17+	"github.com/picosh/pico/pkg/shared"
18+	"github.com/picosh/pico/pkg/tunkit"
19+	"github.com/picosh/utils"
20+)
21+
22+func StartSshServer(cfg *PgsConfig, killCh chan error) {
23+	host := utils.GetEnv("PGS_HOST", "0.0.0.0")
24+	port := utils.GetEnv("PGS_SSH_PORT", "2222")
25+	promPort := utils.GetEnv("PGS_PROM_PORT", "9222")
26+	logger := cfg.Logger
27+
28+	ctx, cancel := context.WithCancel(context.Background())
29+	defer cancel()
30+
31+	cacheClearingQueue := make(chan string, 100)
32+	handler := NewUploadAssetHandler(
33+		cfg,
34+		cacheClearingQueue,
35+		ctx,
36+	)
37+
38+	sshAuth := shared.NewSshAuthHandler(cfg.DB, logger)
39+
40+	webTunnel := &tunkit.WebTunnelHandler{
41+		Logger:      logger,
42+		HttpHandler: createHttpHandler(cfg),
43+	}
44+
45+	// Create a new SSH server
46+	server, err := pssh.NewSSHServerWithConfig(
47+		ctx,
48+		logger,
49+		"pgs-ssh",
50+		host,
51+		port,
52+		promPort,
53+		sshAuth.PubkeyAuthHandler,
54+		[]pssh.SSHServerMiddleware{
55+			pipe.Middleware(handler, ""),
56+			list.Middleware(handler),
57+			scp.Middleware(handler),
58+			rsync.Middleware(handler),
59+			auth.Middleware(handler),
60+			pssh.PtyMdw(pssh.DeprecatedNotice()),
61+			Middleware(handler),
62+			pssh.LogMiddleware(handler, handler.Cfg.DB),
63+		},
64+		[]pssh.SSHServerMiddleware{
65+			sftp.Middleware(handler),
66+			pssh.LogMiddleware(handler, handler.Cfg.DB),
67+		},
68+		map[string]pssh.SSHServerChannelMiddleware{
69+			"direct-tcpip": tunkit.LocalForwardHandler(webTunnel),
70+		},
71+	)
72+
73+	if err != nil {
74+		logger.Error("failed to create ssh server", "err", err.Error())
75+		os.Exit(1)
76+	}
77+
78+	done := make(chan os.Signal, 1)
79+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
80+	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
81+	go func() {
82+		if err = server.ListenAndServe(); err != nil {
83+			logger.Error("serve", "err", err.Error())
84+			os.Exit(1)
85+		}
86+	}()
87+
88+	exit := func() {
89+		logger.Info("stopping ssh server")
90+		cancel()
91+	}
92+
93+	select {
94+	case <-killCh:
95+		exit()
96+	case <-done:
97+		exit()
98+	}
99+}
R pgs/ssh_test.go => pkg/apps/pgs/ssh_test.go
+5, -4
 1@@ -14,9 +14,9 @@ import (
 2 	"testing"
 3 	"time"
 4 
 5-	"github.com/picosh/pico/db"
 6-	pgsdb "github.com/picosh/pico/pgs/db"
 7-	"github.com/picosh/pico/shared/storage"
 8+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 9+	"github.com/picosh/pico/pkg/db"
10+	"github.com/picosh/pico/pkg/shared/storage"
11 	"github.com/picosh/utils"
12 	"github.com/pkg/sftp"
13 	"github.com/prometheus/client_golang/prometheus"
14@@ -408,7 +408,8 @@ func WriteFileWithSftp(cfg *PgsConfig, conn *ssh.Client) (*os.FileInfo, error) {
15 		cfg.Logger.Error("could not write to file", "err", err)
16 		return nil, err
17 	}
18-	f.Close()
19+
20+	cfg.Logger.Info("closing", "err", f.Close())
21 
22 	// check it's there
23 	fi, err := client.Lstat("test/hello.txt")
R pgs/tunnel.go => pkg/apps/pgs/tunnel.go
+6, -5
 1@@ -6,9 +6,10 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/pssh"
10+	"github.com/picosh/pico/pkg/shared"
11+	"golang.org/x/crypto/ssh"
12 )
13 
14 type TunnelWebRouter struct {
15@@ -33,7 +34,7 @@ func (web *TunnelWebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
16 	web.UserRouter.ServeHTTP(w, r.WithContext(ctx))
17 }
18 
19-type CtxHttpBridge = func(ssh.Context) http.Handler
20+type CtxHttpBridge = func(*pssh.SSHServerConnSession) http.Handler
21 
22 func getInfoFromUser(user string) (string, string) {
23 	if strings.Contains(user, "__") {
24@@ -45,7 +46,7 @@ func getInfoFromUser(user string) (string, string) {
25 }
26 
27 func createHttpHandler(cfg *PgsConfig) CtxHttpBridge {
28-	return func(ctx ssh.Context) http.Handler {
29+	return func(ctx *pssh.SSHServerConnSession) http.Handler {
30 		logger := cfg.Logger
31 		asUser, subdomain := getInfoFromUser(ctx.User())
32 		log := logger.With(
R pgs/uploader.go => pkg/apps/pgs/uploader.go
+41, -40
  1@@ -15,15 +15,13 @@ import (
  2 	"sync"
  3 	"time"
  4 
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7-	"github.com/picosh/pico/db"
  8-	pgsdb "github.com/picosh/pico/pgs/db"
  9-	"github.com/picosh/pico/shared"
 10-	wsh "github.com/picosh/pico/wish"
 11-	"github.com/picosh/pobj"
 12-	sst "github.com/picosh/pobj/storage"
 13-	sendutils "github.com/picosh/send/utils"
 14+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
 15+	"github.com/picosh/pico/pkg/db"
 16+	"github.com/picosh/pico/pkg/pobj"
 17+	sst "github.com/picosh/pico/pkg/pobj/storage"
 18+	"github.com/picosh/pico/pkg/pssh"
 19+	sendutils "github.com/picosh/pico/pkg/send/utils"
 20+	"github.com/picosh/pico/pkg/shared"
 21 	"github.com/picosh/utils"
 22 	ignore "github.com/sabhiram/go-gitignore"
 23 )
 24@@ -37,7 +35,7 @@ type DenyList struct {
 25 	Denylist string
 26 }
 27 
 28-func getDenylist(s ssh.Session) *DenyList {
 29+func getDenylist(s *pssh.SSHServerConnSession) *DenyList {
 30 	v := s.Context().Value(ctxDenylistKey{})
 31 	if v == nil {
 32 		return nil
 33@@ -46,11 +44,11 @@ func getDenylist(s ssh.Session) *DenyList {
 34 	return denylist
 35 }
 36 
 37-func setDenylist(s ssh.Session, denylist string) {
 38-	s.Context().SetValue(ctxDenylistKey{}, &DenyList{Denylist: denylist})
 39+func setDenylist(s *pssh.SSHServerConnSession, denylist string) {
 40+	s.SetValue(ctxDenylistKey{}, &DenyList{Denylist: denylist})
 41 }
 42 
 43-func getProject(s ssh.Session) *db.Project {
 44+func getProject(s *pssh.SSHServerConnSession) *db.Project {
 45 	v := s.Context().Value(ctxProjectKey{})
 46 	if v == nil {
 47 		return nil
 48@@ -59,11 +57,11 @@ func getProject(s ssh.Session) *db.Project {
 49 	return project
 50 }
 51 
 52-func setProject(s ssh.Session, project *db.Project) {
 53-	s.Context().SetValue(ctxProjectKey{}, project)
 54+func setProject(s *pssh.SSHServerConnSession, project *db.Project) {
 55+	s.SetValue(ctxProjectKey{}, project)
 56 }
 57 
 58-func getBucket(s ssh.Session) (sst.Bucket, error) {
 59+func getBucket(s *pssh.SSHServerConnSession) (sst.Bucket, error) {
 60 	bucket := s.Context().Value(ctxBucketKey{}).(sst.Bucket)
 61 	if bucket.Name == "" {
 62 		return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
 63@@ -71,11 +69,11 @@ func getBucket(s ssh.Session) (sst.Bucket, error) {
 64 	return bucket, nil
 65 }
 66 
 67-func getStorageSize(s ssh.Session) uint64 {
 68+func getStorageSize(s *pssh.SSHServerConnSession) uint64 {
 69 	return s.Context().Value(ctxStorageSizeKey{}).(uint64)
 70 }
 71 
 72-func incrementStorageSize(s ssh.Session, fileSize int64) uint64 {
 73+func incrementStorageSize(s *pssh.SSHServerConnSession, fileSize int64) uint64 {
 74 	curSize := getStorageSize(s)
 75 	var nextStorageSize uint64
 76 	if fileSize < 0 {
 77@@ -83,7 +81,7 @@ func incrementStorageSize(s ssh.Session, fileSize int64) uint64 {
 78 	} else {
 79 		nextStorageSize = curSize + uint64(fileSize)
 80 	}
 81-	s.Context().SetValue(ctxStorageSizeKey{}, nextStorageSize)
 82+	s.SetValue(ctxStorageSizeKey{}, nextStorageSize)
 83 	return nextStorageSize
 84 }
 85 
 86@@ -113,13 +111,13 @@ func NewUploadAssetHandler(cfg *PgsConfig, ch chan string, ctx context.Context)
 87 	}
 88 }
 89 
 90-func (h *UploadAssetHandler) GetLogger(s ssh.Session) *slog.Logger {
 91-	return wsh.GetLogger(s)
 92+func (h *UploadAssetHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
 93+	return pssh.GetLogger(s)
 94 }
 95 
 96-func (h *UploadAssetHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
 97-	logger := wsh.GetLogger(s)
 98-	user := wsh.GetUser(s)
 99+func (h *UploadAssetHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
100+	logger := pssh.GetLogger(s)
101+	user := pssh.GetUser(s)
102 
103 	if user == nil {
104 		err := fmt.Errorf("could not get user from ctx")
105@@ -153,11 +151,11 @@ func (h *UploadAssetHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os
106 	return fileInfo, reader, nil
107 }
108 
109-func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
110+func (h *UploadAssetHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
111 	var fileList []os.FileInfo
112 
113-	logger := wsh.GetLogger(s)
114-	user := wsh.GetUser(s)
115+	logger := pssh.GetLogger(s)
116+	user := pssh.GetUser(s)
117 
118 	if user == nil {
119 		err := fmt.Errorf("could not get user from ctx")
120@@ -201,9 +199,9 @@ func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool, recur
121 	return fileList, nil
122 }
123 
124-func (h *UploadAssetHandler) Validate(s ssh.Session) error {
125-	logger := wsh.GetLogger(s)
126-	user := wsh.GetUser(s)
127+func (h *UploadAssetHandler) Validate(s *pssh.SSHServerConnSession) error {
128+	logger := pssh.GetLogger(s)
129+	user := pssh.GetUser(s)
130 
131 	if user == nil {
132 		err := fmt.Errorf("could not get user from ctx")
133@@ -217,14 +215,14 @@ func (h *UploadAssetHandler) Validate(s ssh.Session) error {
134 		return err
135 	}
136 
137-	s.Context().SetValue(ctxBucketKey{}, bucket)
138+	s.SetValue(ctxBucketKey{}, bucket)
139 
140 	totalStorageSize, err := h.Cfg.Storage.GetBucketQuota(bucket)
141 	if err != nil {
142 		return err
143 	}
144 
145-	s.Context().SetValue(ctxStorageSizeKey{}, totalStorageSize)
146+	s.SetValue(ctxStorageSizeKey{}, totalStorageSize)
147 
148 	logger.Info(
149 		"bucket size",
150@@ -279,9 +277,9 @@ func findPlusFF(dbpool pgsdb.PgsDB, cfg *PgsConfig, userID string) *db.FeatureFl
151 	return ff
152 }
153 
154-func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
155-	logger := wsh.GetLogger(s)
156-	user := wsh.GetUser(s)
157+func (h *UploadAssetHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
158+	logger := pssh.GetLogger(s)
159+	user := pssh.GetUser(s)
160 
161 	if user == nil {
162 		err := fmt.Errorf("could not get user from ctx")
163@@ -380,7 +378,10 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (s
164 	remaining := int64(storageMax) - int64(curStorageSize)
165 	sizeRemaining := min(remaining+curFileSize, fileMax)
166 	if sizeRemaining <= 0 {
167-		wish.Fatalln(s, "storage quota reached")
168+		fmt.Fprintln(s.Stderr(), "storage quota reached")
169+		fmt.Fprintf(s.Stderr(), "\r")
170+		_ = s.Exit(1)
171+		_ = s.Close()
172 		return "", fmt.Errorf("storage quota reached")
173 	}
174 	logger = logger.With(
175@@ -442,9 +443,9 @@ func isSpecialFile(entry *sendutils.FileEntry) bool {
176 	return fname == "_headers" || fname == "_redirects"
177 }
178 
179-func (h *UploadAssetHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
180-	logger := wsh.GetLogger(s)
181-	user := wsh.GetUser(s)
182+func (h *UploadAssetHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
183+	logger := pssh.GetLogger(s)
184+	user := pssh.GetUser(s)
185 
186 	if user == nil {
187 		err := fmt.Errorf("could not get user from ctx")
188@@ -537,7 +538,7 @@ func (h *UploadAssetHandler) validateAsset(data *FileData) (bool, error) {
189 	return true, nil
190 }
191 
192-func (h *UploadAssetHandler) writeAsset(s ssh.Session, reader io.Reader, data *FileData) (int64, error) {
193+func (h *UploadAssetHandler) writeAsset(s *pssh.SSHServerConnSession, reader io.Reader, data *FileData) (int64, error) {
194 	assetFilepath := shared.GetAssetFileName(data.FileEntry)
195 
196 	logger := h.GetLogger(s)
R pgs/web.go => pkg/apps/pgs/web.go
+4, -4
 1@@ -20,10 +20,10 @@ import (
 2 	"github.com/darkweak/souin/plugins/souin/storages"
 3 	"github.com/darkweak/storages/core"
 4 	"github.com/gorilla/feeds"
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/shared"
 7-	"github.com/picosh/pico/shared/storage"
 8-	sst "github.com/picosh/pobj/storage"
 9+	"github.com/picosh/pico/pkg/db"
10+	sst "github.com/picosh/pico/pkg/pobj/storage"
11+	"github.com/picosh/pico/pkg/shared"
12+	"github.com/picosh/pico/pkg/shared/storage"
13 	"github.com/prometheus/client_golang/prometheus/promhttp"
14 	"google.golang.org/protobuf/proto"
15 )
R pgs/web_asset_handler.go => pkg/apps/pgs/web_asset_handler.go
+2, -2
 1@@ -14,8 +14,8 @@ import (
 2 	"net/http/httputil"
 3 	_ "net/http/pprof"
 4 
 5-	"github.com/picosh/pico/shared/storage"
 6-	sst "github.com/picosh/pobj/storage"
 7+	sst "github.com/picosh/pico/pkg/pobj/storage"
 8+	"github.com/picosh/pico/pkg/shared/storage"
 9 )
10 
11 type ApiAssetHandler struct {
R pgs/web_cache.go => pkg/apps/pgs/web_cache.go
+1, -1
1@@ -6,7 +6,7 @@ import (
2 	"log/slog"
3 	"time"
4 
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils/pipe"
8 )
9 
R pgs/web_test.go => pkg/apps/pgs/web_test.go
+4, -4
 1@@ -10,10 +10,10 @@ import (
 2 	"testing"
 3 	"time"
 4 
 5-	pgsdb "github.com/picosh/pico/pgs/db"
 6-	"github.com/picosh/pico/shared"
 7-	"github.com/picosh/pico/shared/storage"
 8-	sst "github.com/picosh/pobj/storage"
 9+	pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
10+	sst "github.com/picosh/pico/pkg/pobj/storage"
11+	"github.com/picosh/pico/pkg/shared"
12+	"github.com/picosh/pico/pkg/shared/storage"
13 )
14 
15 type ApiExample struct {
R pico/cli.go => pkg/apps/pico/cli.go
+33, -34
  1@@ -8,16 +8,15 @@ import (
  2 	"log/slog"
  3 	"strings"
  4 
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7-	"github.com/picosh/pico/db"
  8-	"github.com/picosh/pico/shared"
  9+	"github.com/picosh/pico/pkg/db"
 10+	"github.com/picosh/pico/pkg/pssh"
 11+	"github.com/picosh/pico/pkg/shared"
 12 	"github.com/picosh/utils"
 13 
 14 	pipeLogger "github.com/picosh/utils/pipe/log"
 15 )
 16 
 17-func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 18+func getUser(s *pssh.SSHServerConnSession, dbpool db.DB) (*db.User, error) {
 19 	if s.PublicKey() == nil {
 20 		return nil, fmt.Errorf("key not found")
 21 	}
 22@@ -38,7 +37,7 @@ func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 23 
 24 type Cmd struct {
 25 	User       *db.User
 26-	SshSession ssh.Session
 27+	SshSession *pssh.SSHServerConnSession
 28 	Session    utils.CmdSession
 29 	Log        *slog.Logger
 30 	Dbpool     db.DB
 31@@ -77,7 +76,7 @@ func (c *Cmd) logs(ctx context.Context) error {
 32 		user := utils.AnyToStr(parsedData, "user")
 33 		userId := utils.AnyToStr(parsedData, "userId")
 34 		if user == c.User.Name || userId == c.User.ID {
 35-			wish.Println(c.SshSession, line)
 36+			fmt.Fprintln(c.SshSession, line)
 37 		}
 38 	}
 39 	return scanner.Err()
 40@@ -88,34 +87,34 @@ type CliHandler struct {
 41 	Logger *slog.Logger
 42 }
 43 
 44-func WishMiddleware(handler *CliHandler) wish.Middleware {
 45+func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
 46 	dbpool := handler.DBPool
 47 	log := handler.Logger
 48 
 49-	return func(next ssh.Handler) ssh.Handler {
 50-		return func(sesh ssh.Session) {
 51+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 52+		return func(sesh *pssh.SSHServerConnSession) error {
 53 			args := sesh.Command()
 54 			if len(args) == 0 {
 55-				next(sesh)
 56-				return
 57+				return next(sesh)
 58 			}
 59 
 60 			user, err := getUser(sesh, dbpool)
 61 			if err != nil {
 62-				wish.Errorf(sesh, "detected ssh command: %s\n", args)
 63+				fmt.Fprintf(sesh.Stderr(), "detected ssh command: %s\n", args)
 64 				s := fmt.Errorf("error: you need to create an account before using the remote cli: %w", err)
 65-				wish.Fatalln(sesh, s)
 66-				return
 67+				sesh.Fatal(s)
 68+				return s
 69 			}
 70 
 71 			if len(args) > 0 && args[0] == "chat" {
 72 				_, _, hasPty := sesh.Pty()
 73 				if !hasPty {
 74-					wish.Fatalln(
 75-						sesh,
 76-						"In order to render chat you need to enable PTY with the `ssh -t` flag",
 77+					err := fmt.Errorf(
 78+						"in order to render chat you need to enable PTY with the `ssh -t` flag",
 79 					)
 80-					return
 81+
 82+					sesh.Fatal(err)
 83+					return err
 84 				}
 85 
 86 				ff, err := dbpool.FindFeatureForUser(user.ID, "plus")
 87@@ -124,29 +123,30 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
 88 					ff, err = dbpool.FindFeatureForUser(user.ID, "bouncer")
 89 					if err != nil {
 90 						handler.Logger.Error("Unable to find bouncer feature flag", "err", err, "user", user, "command", args)
 91-						wish.Fatalln(sesh, "Unable to find plus or bouncer feature flag")
 92-						return
 93+						sesh.Fatal(err)
 94+						return err
 95 					}
 96 				}
 97 
 98 				if ff == nil {
 99-					wish.Fatalln(sesh, "Unable to find plus or bouncer feature flag")
100-					return
101+					err = fmt.Errorf("unable to find plus or bouncer feature flag")
102+					sesh.Fatal(err)
103+					return err
104 				}
105 
106 				pass, err := dbpool.UpsertToken(user.ID, "pico-chat")
107 				if err != nil {
108-					wish.Fatalln(sesh, err)
109-					return
110+					sesh.Fatal(err)
111+					return err
112 				}
113 				app, err := shared.NewSenpaiApp(sesh, user.Name, pass)
114 				if err != nil {
115-					wish.Fatalln(sesh, err)
116-					return
117+					sesh.Fatal(err)
118+					return err
119 				}
120 				app.Run()
121 				app.Close()
122-				return
123+				return err
124 			}
125 
126 			opts := Cmd{
127@@ -162,20 +162,19 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
128 			if len(args) == 1 {
129 				if cmd == "help" {
130 					opts.help()
131-					return
132+					return nil
133 				} else if cmd == "logs" {
134 					err = opts.logs(sesh.Context())
135 					if err != nil {
136-						wish.Fatalln(sesh, err)
137+						sesh.Fatal(err)
138 					}
139-					return
140+					return nil
141 				} else {
142-					next(sesh)
143-					return
144+					return next(sesh)
145 				}
146 			}
147 
148-			next(sesh)
149+			return next(sesh)
150 		}
151 	}
152 }
R pico/config.go => pkg/apps/pico/config.go
+1, -1
1@@ -1,7 +1,7 @@
2 package pico
3 
4 import (
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils"
8 )
9 
R pico/file_handler.go => pkg/apps/pico/file_handler.go
+28, -29
  1@@ -11,13 +11,12 @@ import (
  2 	"strings"
  3 	"time"
  4 
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7-	"github.com/picosh/pico/db"
  8-	"github.com/picosh/pico/shared"
  9-	wsh "github.com/picosh/pico/wish"
 10-	sendutils "github.com/picosh/send/utils"
 11+	"github.com/picosh/pico/pkg/db"
 12+	"github.com/picosh/pico/pkg/pssh"
 13+	sendutils "github.com/picosh/pico/pkg/send/utils"
 14+	"github.com/picosh/pico/pkg/shared"
 15 	"github.com/picosh/utils"
 16+	"golang.org/x/crypto/ssh"
 17 )
 18 
 19 type UploadHandler struct {
 20@@ -52,13 +51,13 @@ func (h *UploadHandler) getAuthorizedKeyFile(user *db.User) (*sendutils.VirtualF
 21 	return fileInfo, text, nil
 22 }
 23 
 24-func (h *UploadHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
 25+func (h *UploadHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
 26 	return errors.New("unsupported")
 27 }
 28 
 29-func (h *UploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
 30-	logger := wsh.GetLogger(s)
 31-	user := wsh.GetUser(s)
 32+func (h *UploadHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
 33+	logger := pssh.GetLogger(s)
 34+	user := pssh.GetUser(s)
 35 
 36 	if user == nil {
 37 		err := fmt.Errorf("could not get user from ctx")
 38@@ -84,11 +83,11 @@ func (h *UploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.File
 39 	return nil, nil, os.ErrNotExist
 40 }
 41 
 42-func (h *UploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
 43+func (h *UploadHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
 44 	var fileList []os.FileInfo
 45 
 46-	logger := wsh.GetLogger(s)
 47-	user := wsh.GetUser(s)
 48+	logger := pssh.GetLogger(s)
 49+	user := pssh.GetUser(s)
 50 
 51 	if user == nil {
 52 		err := fmt.Errorf("could not get user from ctx")
 53@@ -127,11 +126,11 @@ func (h *UploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive
 54 	return fileList, nil
 55 }
 56 
 57-func (h *UploadHandler) GetLogger(s ssh.Session) *slog.Logger {
 58-	return wsh.GetLogger(s)
 59+func (h *UploadHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
 60+	return pssh.GetLogger(s)
 61 }
 62 
 63-func (h *UploadHandler) Validate(s ssh.Session) error {
 64+func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
 65 	var err error
 66 	key, err := sendutils.KeyText(s)
 67 	if err != nil {
 68@@ -169,7 +168,7 @@ func authorizedKeysDiff(keyInUse ssh.PublicKey, curKeys []KeyWithId, nextKeys []
 69 	for _, nk := range nextKeys {
 70 		found := false
 71 		for _, ck := range curKeys {
 72-			if ssh.KeysEqual(nk.Pk, ck.Pk) {
 73+			if pssh.KeysEqual(nk.Pk, ck.Pk) {
 74 				found = true
 75 
 76 				// update the comment field
 77@@ -189,13 +188,13 @@ func authorizedKeysDiff(keyInUse ssh.PublicKey, curKeys []KeyWithId, nextKeys []
 78 	for _, ck := range curKeys {
 79 		// we never want to remove the key that's in the current ssh session
 80 		// in an effort to avoid mistakenly removing their current key
 81-		if ssh.KeysEqual(ck.Pk, keyInUse) {
 82+		if pssh.KeysEqual(ck.Pk, keyInUse) {
 83 			continue
 84 		}
 85 
 86 		found := false
 87 		for _, nk := range nextKeys {
 88-			if ssh.KeysEqual(ck.Pk, nk.Pk) {
 89+			if pssh.KeysEqual(ck.Pk, nk.Pk) {
 90 				found = true
 91 				break
 92 			}
 93@@ -212,7 +211,7 @@ func authorizedKeysDiff(keyInUse ssh.PublicKey, curKeys []KeyWithId, nextKeys []
 94 	}
 95 }
 96 
 97-func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger, user *db.User, s ssh.Session) error {
 98+func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger, user *db.User, s *pssh.SSHServerConnSession) error {
 99 	logger.Info("processing new authorized_keys")
100 	dbpool := h.DBPool
101 
102@@ -245,12 +244,12 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
103 	for _, pk := range diff.Add {
104 		key := utils.KeyForKeyText(pk.Pk)
105 
106-		wish.Errorf(s, "adding pubkey (%s)\n", key)
107+		fmt.Fprintf(s.Stderr(), "adding pubkey (%s)\n", key)
108 		logger.Info("adding pubkey", "pubkey", key)
109 
110 		err = dbpool.InsertPublicKey(user.ID, key, pk.Comment, nil)
111 		if err != nil {
112-			wish.Errorf(s, "error: could not insert pubkey: %s (%s)\n", err.Error(), key)
113+			fmt.Fprintf(s.Stderr(), "error: could not insert pubkey: %s (%s)\n", err.Error(), key)
114 			logger.Error("could not insert pubkey", "err", err.Error())
115 		}
116 	}
117@@ -258,7 +257,7 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
118 	for _, pk := range diff.Update {
119 		key := utils.KeyForKeyText(pk.Pk)
120 
121-		wish.Errorf(s, "updating pubkey with comment: %s (%s)\n", pk.Comment, key)
122+		fmt.Fprintf(s.Stderr(), "updating pubkey with comment: %s (%s)\n", pk.Comment, key)
123 		logger.Info(
124 			"updating pubkey with comment",
125 			"pubkey", key,
126@@ -267,18 +266,18 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
127 
128 		_, err = dbpool.UpdatePublicKey(pk.ID, pk.Comment)
129 		if err != nil {
130-			wish.Errorf(s, "error: could not update pubkey: %s (%s)\n", err.Error(), key)
131+			fmt.Fprintf(s.Stderr(), "error: could not update pubkey: %s (%s)\n", err.Error(), key)
132 			logger.Error("could not update pubkey", "err", err.Error(), "key", key)
133 		}
134 	}
135 
136 	if len(diff.Rm) > 0 {
137-		wish.Errorf(s, "removing pubkeys: %s\n", diff.Rm)
138+		fmt.Fprintf(s.Stderr(), "removing pubkeys: %s\n", diff.Rm)
139 		logger.Info("removing pubkeys", "pubkeys", diff.Rm)
140 
141 		err = dbpool.RemoveKeys(diff.Rm)
142 		if err != nil {
143-			wish.Errorf(s, "error: could not rm pubkeys: %s\n", err.Error())
144+			fmt.Fprintf(s.Stderr(), "error: could not rm pubkeys: %s\n", err.Error())
145 			logger.Error("could not remove pubkey", "err", err.Error())
146 		}
147 	}
148@@ -286,9 +285,9 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
149 	return nil
150 }
151 
152-func (h *UploadHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
153-	logger := wsh.GetLogger(s)
154-	user := wsh.GetUser(s)
155+func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
156+	logger := pssh.GetLogger(s)
157+	user := pssh.GetUser(s)
158 
159 	if user == nil {
160 		err := fmt.Errorf("could not get user from ctx")
R pico/html/.gitkeep => pkg/apps/pico/html/.gitkeep
+0, -0
A pkg/apps/pico/ssh.go
+136, -0
  1@@ -0,0 +1,136 @@
  2+package pico
  3+
  4+import (
  5+	"context"
  6+	"os"
  7+	"os/signal"
  8+	"syscall"
  9+
 10+	"git.sr.ht/~rockorager/vaxis"
 11+	"github.com/picosh/pico/pkg/db/postgres"
 12+	"github.com/picosh/pico/pkg/pssh"
 13+	"github.com/picosh/pico/pkg/send/auth"
 14+	"github.com/picosh/pico/pkg/send/list"
 15+	"github.com/picosh/pico/pkg/send/pipe"
 16+	"github.com/picosh/pico/pkg/send/protocols/rsync"
 17+	"github.com/picosh/pico/pkg/send/protocols/scp"
 18+	"github.com/picosh/pico/pkg/send/protocols/sftp"
 19+	"github.com/picosh/pico/pkg/shared"
 20+	"github.com/picosh/pico/pkg/tui"
 21+	"github.com/picosh/utils"
 22+	"golang.org/x/crypto/ssh"
 23+)
 24+
 25+func createTui(shrd *tui.SharedModel) pssh.SSHServerMiddleware {
 26+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 27+		return func(sesh *pssh.SSHServerConnSession) error {
 28+			vty, err := shared.NewVConsole(sesh)
 29+			if err != nil {
 30+				return err
 31+			}
 32+			opts := vaxis.Options{
 33+				WithConsole: vty,
 34+			}
 35+			tui.NewTui(opts, shrd)
 36+			return nil
 37+		}
 38+	}
 39+}
 40+
 41+func StartSshServer() {
 42+	appName := "pico-ssh"
 43+
 44+	host := utils.GetEnv("PICO_HOST", "0.0.0.0")
 45+	port := utils.GetEnv("PICO_SSH_PORT", "2222")
 46+	promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
 47+	cfg := NewConfigSite(appName)
 48+	logger := cfg.Logger
 49+
 50+	ctx, cancel := context.WithCancel(context.Background())
 51+	defer cancel()
 52+
 53+	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
 54+	defer dbpool.Close()
 55+
 56+	handler := NewUploadHandler(
 57+		dbpool,
 58+		cfg,
 59+	)
 60+
 61+	cliHandler := &CliHandler{
 62+		Logger: logger,
 63+		DBPool: dbpool,
 64+	}
 65+
 66+	sshAuth := shared.NewSshAuthHandler(dbpool, logger)
 67+
 68+	// Create a new SSH server
 69+	server, err := pssh.NewSSHServerWithConfig(
 70+		ctx,
 71+		logger,
 72+		appName,
 73+		host,
 74+		port,
 75+		promPort,
 76+		func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
 77+			perms, _ := sshAuth.PubkeyAuthHandler(conn, key)
 78+			if perms == nil {
 79+				perms = &ssh.Permissions{
 80+					Extensions: map[string]string{
 81+						"pubkey": utils.KeyForKeyText(key),
 82+					},
 83+				}
 84+			}
 85+
 86+			return perms, nil
 87+		},
 88+		[]pssh.SSHServerMiddleware{
 89+			pipe.Middleware(handler, ""),
 90+			list.Middleware(handler),
 91+			scp.Middleware(handler),
 92+			rsync.Middleware(handler),
 93+			auth.Middleware(handler),
 94+			func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 95+				return func(sesh *pssh.SSHServerConnSession) error {
 96+					shrd := &tui.SharedModel{
 97+						Session: sesh,
 98+						Cfg:     cfg,
 99+						Dbpool:  handler.DBPool,
100+						Logger:  cfg.Logger,
101+					}
102+					return pssh.PtyMdw(createTui(shrd))(next)(sesh)
103+				}
104+			},
105+			Middleware(cliHandler),
106+			pssh.LogMiddleware(handler, dbpool),
107+		},
108+		[]pssh.SSHServerMiddleware{
109+			sftp.Middleware(handler),
110+			pssh.LogMiddleware(handler, dbpool),
111+		},
112+		nil,
113+	)
114+
115+	if err != nil {
116+		logger.Error("failed to create ssh server", "err", err.Error())
117+		os.Exit(1)
118+	}
119+
120+	done := make(chan os.Signal, 1)
121+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
122+	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
123+	go func() {
124+		if err = server.ListenAndServe(); err != nil {
125+			logger.Error("serve", "err", err.Error())
126+			os.Exit(1)
127+		}
128+	}()
129+
130+	exit := func() {
131+		logger.Info("stopping ssh server")
132+		cancel()
133+	}
134+
135+	<-done
136+	exit()
137+}
R pipe/api.go => pkg/apps/pipe/api.go
+2, -2
 1@@ -16,8 +16,8 @@ import (
 2 
 3 	"github.com/google/uuid"
 4 	"github.com/gorilla/websocket"
 5-	"github.com/picosh/pico/db/postgres"
 6-	"github.com/picosh/pico/shared"
 7+	"github.com/picosh/pico/pkg/db/postgres"
 8+	"github.com/picosh/pico/pkg/shared"
 9 	"github.com/picosh/utils/pipe"
10 	"github.com/prometheus/client_golang/prometheus/promhttp"
11 )
R pipe/cli.go => pkg/apps/pipe/cli.go
+44, -40
  1@@ -13,17 +13,15 @@ import (
  2 	"time"
  3 
  4 	"github.com/antoniomika/syncmap"
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7 	"github.com/google/uuid"
  8-	"github.com/picosh/pico/db"
  9-	"github.com/picosh/pico/shared"
 10-	wsh "github.com/picosh/pico/wish"
 11+	"github.com/picosh/pico/pkg/db"
 12+	"github.com/picosh/pico/pkg/pssh"
 13+	"github.com/picosh/pico/pkg/shared"
 14 	psub "github.com/picosh/pubsub"
 15 	gossh "golang.org/x/crypto/ssh"
 16 )
 17 
 18-func flagSet(cmdName string, sesh ssh.Session) *flag.FlagSet {
 19+func flagSet(cmdName string, sesh *pssh.SSHServerConnSession) *flag.FlagSet {
 20 	cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
 21 	cmd.SetOutput(sesh)
 22 	cmd.Usage = func() {
 23@@ -73,7 +71,7 @@ func clientInfo(clients []*psub.Client, clientType string) string {
 24 }
 25 
 26 var helpStr = func(sshCmd string) string {
 27-	return fmt.Sprintf(`Command: ssh %s <help | ls | pub | sub | pipe> <topic> [-h | args...]
 28+	data := fmt.Sprintf(`Command: ssh %s <help | ls | pub | sub | pipe> <topic> [-h | args...]
 29 
 30 The simplest authenticated pubsub system.  Send messages through
 31 user-defined topics.  Topics are private to the authenticated
 32@@ -91,6 +89,10 @@ data is being sent:
 33 - sub => reads from client
 34 - pipe => read and write between clients
 35 `, sshCmd)
 36+
 37+	data = strings.ReplaceAll(data, "\n", "\r\n")
 38+
 39+	return data
 40 }
 41 
 42 type CliHandler struct {
 43@@ -102,6 +104,10 @@ type CliHandler struct {
 44 	Access  *syncmap.Map[string, []string]
 45 }
 46 
 47+func (h *CliHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
 48+	return h.Logger
 49+}
 50+
 51 func toSshCmd(cfg *shared.ConfigSite) string {
 52 	port := ""
 53 	if cfg.PortOverride != "22" {
 54@@ -120,7 +126,7 @@ func parseArgList(arg string) []string {
 55 }
 56 
 57 // checkAccess checks if the user has access to a topic based on an access list.
 58-func checkAccess(accessList []string, userName string, sesh ssh.Session) bool {
 59+func checkAccess(accessList []string, userName string, sesh *pssh.SSHServerConnSession) bool {
 60 	for _, acc := range accessList {
 61 		if acc == userName {
 62 			return true
 63@@ -134,21 +140,20 @@ func checkAccess(accessList []string, userName string, sesh ssh.Session) bool {
 64 	return false
 65 }
 66 
 67-func WishMiddleware(handler *CliHandler) wish.Middleware {
 68+func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
 69 	pubsub := handler.PubSub
 70 
 71-	return func(next ssh.Handler) ssh.Handler {
 72-		return func(sesh ssh.Session) {
 73+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 74+		return func(sesh *pssh.SSHServerConnSession) error {
 75 			ctx := sesh.Context()
 76-			logger := wsh.GetLogger(sesh)
 77-			user := wsh.GetUser(sesh)
 78+			logger := pssh.GetLogger(sesh)
 79+			user := pssh.GetUser(sesh)
 80 
 81 			args := sesh.Command()
 82 
 83 			if len(args) == 0 {
 84-				wish.Println(sesh, helpStr(toSshCmd(handler.Cfg)))
 85-				next(sesh)
 86-				return
 87+				fmt.Fprintln(sesh, helpStr(toSshCmd(handler.Cfg)))
 88+				return next(sesh)
 89 			}
 90 
 91 			userName := "public"
 92@@ -188,13 +193,13 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
 93 
 94 			cmd := strings.TrimSpace(args[0])
 95 			if cmd == "help" {
 96-				wish.Println(sesh, helpStr(toSshCmd(handler.Cfg)))
 97-				next(sesh)
 98-				return
 99+				fmt.Fprintln(sesh, helpStr(toSshCmd(handler.Cfg)))
100+				return next(sesh)
101 			} else if cmd == "ls" {
102 				if userName == "public" {
103-					wish.Fatalln(sesh, "access denied")
104-					return
105+					err := fmt.Errorf("access denied")
106+					sesh.Fatal(err)
107+					return err
108 				}
109 
110 				topicFilter := fmt.Sprintf("%s/", userName)
111@@ -221,7 +226,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
112 				}
113 
114 				if len(channels) == 0 && len(waitingChannels) == 0 {
115-					wish.Println(sesh, "no pubsub channels found")
116+					fmt.Fprintln(sesh, "no pubsub channels found")
117 				} else {
118 					var outputData string
119 					if len(channels) > 0 || len(waitingChannels) > 0 {
120@@ -273,8 +278,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
121 					_, _ = sesh.Write([]byte(outputData))
122 				}
123 
124-				next(sesh)
125-				return
126+				return next(sesh)
127 			}
128 
129 			topic := ""
130@@ -306,7 +310,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
131 				clean := pubCmd.Bool("c", false, "Don't send status messages")
132 
133 				if !flagCheck(pubCmd, topic, cmdArgs) {
134-					return
135+					return err
136 				}
137 
138 				if pubCmd.NArg() == 1 && topic == "" {
139@@ -382,7 +386,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
140 				}
141 
142 				if !*clean {
143-					wish.Printf(
144+					fmt.Fprintf(
145 						sesh,
146 						"subscribe to this channel:\n  ssh %s sub %s%s\n",
147 						toSshCmd(handler.Cfg),
148@@ -415,7 +419,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
149 						}
150 
151 						if !*clean {
152-							wish.Println(sesh, termMsg)
153+							fmt.Fprintln(sesh, termMsg)
154 						}
155 
156 						ready := make(chan struct{})
157@@ -454,7 +458,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
158 							cancel()
159 
160 							if !*clean {
161-								wish.Fatalln(sesh, "timeout reached, exiting ...")
162+								sesh.Fatal(fmt.Errorf("timeout reached, exiting"))
163 							} else {
164 								err = sesh.Exit(1)
165 								if err != nil {
166@@ -486,7 +490,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
167 				}
168 
169 				if !*clean {
170-					wish.Println(sesh, "sending msg ...")
171+					fmt.Fprintln(sesh, "sending msg ...")
172 				}
173 
174 				err = pubsub.Pub(
175@@ -500,11 +504,11 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
176 				)
177 
178 				if !*clean {
179-					wish.Println(sesh, "msg sent!")
180+					fmt.Fprintln(sesh, "msg sent!")
181 				}
182 
183 				if err != nil && !*clean {
184-					wish.Errorln(sesh, err)
185+					fmt.Fprintln(sesh.Stderr(), err)
186 				}
187 			} else if cmd == "sub" {
188 				subCmd := flagSet("sub", sesh)
189@@ -514,7 +518,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
190 				clean := subCmd.Bool("c", false, "Don't send status messages")
191 
192 				if !flagCheck(subCmd, topic, cmdArgs) {
193-					return
194+					return err
195 				}
196 
197 				if subCmd.NArg() == 1 && topic == "" {
198@@ -569,8 +573,8 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
199 					} else if !*public {
200 						name = toTopic(userName, withoutUser)
201 					} else {
202-						wish.Errorln(sesh, "access denied")
203-						return
204+						fmt.Fprintln(sesh.Stderr(), "access denied")
205+						return err
206 					}
207 				}
208 
209@@ -585,7 +589,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
210 				)
211 
212 				if err != nil && !*clean {
213-					wish.Errorln(sesh, err)
214+					fmt.Fprintln(sesh.Stderr(), err)
215 				}
216 			} else if cmd == "pipe" {
217 				pipeCmd := flagSet("pipe", sesh)
218@@ -595,7 +599,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
219 				clean := pipeCmd.Bool("c", false, "Don't send status messages")
220 
221 				if !flagCheck(pipeCmd, topic, cmdArgs) {
222-					return
223+					return err
224 				}
225 
226 				if pipeCmd.NArg() == 1 && topic == "" {
227@@ -663,7 +667,7 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
228 				}
229 
230 				if isCreator && !*clean {
231-					wish.Printf(
232+					fmt.Fprintf(
233 						sesh,
234 						"subscribe to this topic:\n  ssh %s sub %s%s\n",
235 						toSshCmd(handler.Cfg),
236@@ -683,15 +687,15 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
237 				)
238 
239 				if readErr != nil && !*clean {
240-					wish.Errorln(sesh, "error reading from pipe", readErr)
241+					fmt.Fprintln(sesh.Stderr(), "error reading from pipe", readErr)
242 				}
243 
244 				if writeErr != nil && !*clean {
245-					wish.Errorln(sesh, "error writing to pipe", writeErr)
246+					fmt.Fprintln(sesh.Stderr(), "error writing to pipe", writeErr)
247 				}
248 			}
249 
250-			next(sesh)
251+			return next(sesh)
252 		}
253 	}
254 }
R pipe/config.go => pkg/apps/pipe/config.go
+1, -1
1@@ -1,7 +1,7 @@
2 package pipe
3 
4 import (
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils"
8 )
9 
R pipe/html/base.layout.tmpl => pkg/apps/pipe/html/base.layout.tmpl
+0, -0
R pipe/html/footer.partial.tmpl => pkg/apps/pipe/html/footer.partial.tmpl
+0, -0
R pipe/html/marketing-footer.partial.tmpl => pkg/apps/pipe/html/marketing-footer.partial.tmpl
+0, -0
R pipe/html/marketing.page.tmpl => pkg/apps/pipe/html/marketing.page.tmpl
+0, -0
R pipe/public/anim.js => pkg/apps/pipe/public/anim.js
+0, -0
R pipe/public/apple-touch-icon.png => pkg/apps/pipe/public/apple-touch-icon.png
+0, -0
R pipe/public/favicon-16x16.png => pkg/apps/pipe/public/favicon-16x16.png
+0, -0
R pipe/public/favicon.ico => pkg/apps/pipe/public/favicon.ico
+0, -0
R pipe/public/robots.txt => pkg/apps/pipe/public/robots.txt
+0, -0
A pkg/apps/pipe/ssh.go
+99, -0
  1@@ -0,0 +1,99 @@
  2+package pipe
  3+
  4+import (
  5+	"context"
  6+	"os"
  7+	"os/signal"
  8+	"syscall"
  9+
 10+	"github.com/antoniomika/syncmap"
 11+	"github.com/picosh/pico/pkg/db/postgres"
 12+	"github.com/picosh/pico/pkg/pssh"
 13+	"github.com/picosh/pico/pkg/shared"
 14+	psub "github.com/picosh/pubsub"
 15+	"github.com/picosh/utils"
 16+	"golang.org/x/crypto/ssh"
 17+)
 18+
 19+func StartSshServer() {
 20+	appName := "pipe-ssh"
 21+
 22+	host := utils.GetEnv("PIPE_HOST", "0.0.0.0")
 23+	port := utils.GetEnv("PIPE_SSH_PORT", "2222")
 24+	portOverride := utils.GetEnv("PIPE_SSH_PORT_OVERRIDE", port)
 25+	promPort := utils.GetEnv("PIPE_PROM_PORT", "9222")
 26+	cfg := NewConfigSite(appName)
 27+	logger := cfg.Logger
 28+
 29+	ctx, cancel := context.WithCancel(context.Background())
 30+	defer cancel()
 31+
 32+	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 33+	defer dbh.Close()
 34+
 35+	cfg.Port = port
 36+	cfg.PortOverride = portOverride
 37+
 38+	pubsub := psub.NewMulticast(logger)
 39+	handler := &CliHandler{
 40+		Logger:  logger,
 41+		DBPool:  dbh,
 42+		PubSub:  pubsub,
 43+		Cfg:     cfg,
 44+		Waiters: syncmap.New[string, []string](),
 45+		Access:  syncmap.New[string, []string](),
 46+	}
 47+
 48+	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 49+
 50+	// Create a new SSH server
 51+	server, err := pssh.NewSSHServerWithConfig(
 52+		ctx,
 53+		logger,
 54+		appName,
 55+		host,
 56+		port,
 57+		promPort,
 58+		func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
 59+			perms, _ := sshAuth.PubkeyAuthHandler(conn, key)
 60+			if perms == nil {
 61+				perms = &ssh.Permissions{
 62+					Extensions: map[string]string{
 63+						"pubkey": utils.KeyForKeyText(key),
 64+					},
 65+				}
 66+			}
 67+
 68+			return perms, nil
 69+		},
 70+		[]pssh.SSHServerMiddleware{
 71+			Middleware(handler),
 72+			pssh.LogMiddleware(handler, dbh),
 73+		},
 74+		nil,
 75+		nil,
 76+	)
 77+
 78+	if err != nil {
 79+		logger.Error("failed to create ssh server", "err", err.Error())
 80+		os.Exit(1)
 81+	}
 82+
 83+	done := make(chan os.Signal, 1)
 84+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 85+	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
 86+	go func() {
 87+		if err = server.ListenAndServe(); err != nil {
 88+			logger.Error("serve", "err", err.Error())
 89+			os.Exit(1)
 90+		}
 91+	}()
 92+
 93+	exit := func() {
 94+		logger.Info("stopping ssh server")
 95+		cancel()
 96+	}
 97+
 98+	<-done
 99+	exit()
100+}
R prose/api.go => pkg/apps/prose/api.go
+4, -4
 1@@ -15,10 +15,10 @@ import (
 2 	"slices"
 3 
 4 	"github.com/gorilla/feeds"
 5-	"github.com/picosh/pico/db"
 6-	"github.com/picosh/pico/db/postgres"
 7-	"github.com/picosh/pico/shared"
 8-	"github.com/picosh/pico/shared/storage"
 9+	"github.com/picosh/pico/pkg/db"
10+	"github.com/picosh/pico/pkg/db/postgres"
11+	"github.com/picosh/pico/pkg/shared"
12+	"github.com/picosh/pico/pkg/shared/storage"
13 	"github.com/picosh/utils"
14 	"github.com/prometheus/client_golang/prometheus/promhttp"
15 )
R prose/artifacts/main.css => pkg/apps/prose/artifacts/main.css
+0, -0
R prose/config.go => pkg/apps/prose/config.go
+1, -1
1@@ -1,7 +1,7 @@
2 package prose
3 
4 import (
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils"
8 )
9 
R prose/html/base.layout.tmpl => pkg/apps/prose/html/base.layout.tmpl
+0, -0
R prose/html/blog-aside.partial.tmpl => pkg/apps/prose/html/blog-aside.partial.tmpl
+0, -0
R prose/html/blog-default.partial.tmpl => pkg/apps/prose/html/blog-default.partial.tmpl
+0, -0
R prose/html/blog.page.tmpl => pkg/apps/prose/html/blog.page.tmpl
+0, -0
R prose/html/footer.partial.tmpl => pkg/apps/prose/html/footer.partial.tmpl
+0, -0
R prose/html/imgs.page.tmpl => pkg/apps/prose/html/imgs.page.tmpl
+0, -0
R prose/html/marketing-footer.partial.tmpl => pkg/apps/prose/html/marketing-footer.partial.tmpl
+0, -0
R prose/html/post.page.tmpl => pkg/apps/prose/html/post.page.tmpl
+0, -0
R prose/html/read.page.tmpl => pkg/apps/prose/html/read.page.tmpl
+0, -0
R prose/html/rss.page.tmpl => pkg/apps/prose/html/rss.page.tmpl
+0, -0
R prose/public/card.png => pkg/apps/prose/public/card.png
+0, -0
R prose/public/favicon-16x16.png => pkg/apps/prose/public/favicon-16x16.png
+0, -0
R prose/public/favicon.ico => pkg/apps/prose/public/favicon.ico
+0, -0
R prose/public/robots.txt => pkg/apps/prose/public/robots.txt
+0, -0
R prose/public/smol-v2.css => pkg/apps/prose/public/smol-v2.css
+0, -0
R prose/public/smol.css => pkg/apps/prose/public/smol.css
+0, -0
R prose/public/syntax.css => pkg/apps/prose/public/syntax.css
+0, -0
R prose/scp_hooks.go => pkg/apps/prose/scp_hooks.go
+6, -6
 1@@ -6,10 +6,10 @@ import (
 2 
 3 	"slices"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/filehandlers"
 8-	"github.com/picosh/pico/shared"
 9+	"github.com/picosh/pico/pkg/db"
10+	"github.com/picosh/pico/pkg/filehandlers"
11+	"github.com/picosh/pico/pkg/pssh"
12+	"github.com/picosh/pico/pkg/shared"
13 	"github.com/picosh/utils"
14 	pipeUtil "github.com/picosh/utils/pipe"
15 )
16@@ -20,7 +20,7 @@ type MarkdownHooks struct {
17 	Pipe *pipeUtil.ReconnectReadWriteCloser
18 }
19 
20-func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
21+func (p *MarkdownHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
22 	if !utils.IsTextFile(data.Text) {
23 		err := fmt.Errorf(
24 			"ERROR: (%s) invalid file must be plain text (utf-8), skipping",
25@@ -57,7 +57,7 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
26 	return true, nil
27 }
28 
29-func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
30+func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
31 	parsedText, err := shared.ParseText(data.Text)
32 	if err != nil {
33 		return fmt.Errorf("%s: %w", data.Filename, err)
A pkg/apps/prose/ssh.go
+113, -0
  1@@ -0,0 +1,113 @@
  2+package prose
  3+
  4+import (
  5+	"context"
  6+	"os"
  7+	"os/signal"
  8+	"syscall"
  9+
 10+	"github.com/picosh/pico/pkg/db/postgres"
 11+	"github.com/picosh/pico/pkg/filehandlers"
 12+	uploadimgs "github.com/picosh/pico/pkg/filehandlers/imgs"
 13+	"github.com/picosh/pico/pkg/pssh"
 14+	"github.com/picosh/pico/pkg/send/auth"
 15+	"github.com/picosh/pico/pkg/send/list"
 16+	"github.com/picosh/pico/pkg/send/pipe"
 17+	"github.com/picosh/pico/pkg/send/protocols/rsync"
 18+	"github.com/picosh/pico/pkg/send/protocols/scp"
 19+	"github.com/picosh/pico/pkg/send/protocols/sftp"
 20+	"github.com/picosh/pico/pkg/shared"
 21+	"github.com/picosh/pico/pkg/shared/storage"
 22+	"github.com/picosh/utils"
 23+)
 24+
 25+func StartSshServer() {
 26+	appName := "prose-ssh"
 27+
 28+	host := utils.GetEnv("PROSE_HOST", "0.0.0.0")
 29+	port := utils.GetEnv("PROSE_SSH_PORT", "2222")
 30+	promPort := utils.GetEnv("PROSE_PROM_PORT", "9222")
 31+	cfg := NewConfigSite(appName)
 32+	logger := cfg.Logger
 33+
 34+	ctx, cancel := context.WithCancel(context.Background())
 35+	defer cancel()
 36+
 37+	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 38+	defer dbh.Close()
 39+
 40+	hooks := &MarkdownHooks{
 41+		Cfg: cfg,
 42+		Db:  dbh,
 43+	}
 44+
 45+	var st storage.StorageServe
 46+	var err error
 47+	if cfg.MinioURL == "" {
 48+		st, err = storage.NewStorageFS(cfg.Logger, cfg.StorageDir)
 49+	} else {
 50+		st, err = storage.NewStorageMinio(cfg.Logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
 51+	}
 52+
 53+	if err != nil {
 54+		logger.Error("storage", "err", err.Error())
 55+		return
 56+	}
 57+
 58+	fileMap := map[string]filehandlers.ReadWriteHandler{
 59+		".md":      filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 60+		".css":     filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 61+		"fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
 62+	}
 63+	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 64+
 65+	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 66+
 67+	// Create a new SSH server
 68+	server, err := pssh.NewSSHServerWithConfig(
 69+		ctx,
 70+		logger,
 71+		appName,
 72+		host,
 73+		port,
 74+		promPort,
 75+		sshAuth.PubkeyAuthHandler,
 76+		[]pssh.SSHServerMiddleware{
 77+			pipe.Middleware(handler, ".md"),
 78+			list.Middleware(handler),
 79+			scp.Middleware(handler),
 80+			rsync.Middleware(handler),
 81+			auth.Middleware(handler),
 82+			pssh.PtyMdw(pssh.DeprecatedNotice()),
 83+			pssh.LogMiddleware(handler, dbh),
 84+		},
 85+		[]pssh.SSHServerMiddleware{
 86+			sftp.Middleware(handler),
 87+			pssh.LogMiddleware(handler, dbh),
 88+		},
 89+		nil,
 90+	)
 91+
 92+	if err != nil {
 93+		logger.Error("failed to create ssh server", "err", err.Error())
 94+		os.Exit(1)
 95+	}
 96+
 97+	done := make(chan os.Signal, 1)
 98+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 99+	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
100+	go func() {
101+		if err = server.ListenAndServe(); err != nil {
102+			logger.Error("serve", "err", err.Error())
103+			os.Exit(1)
104+		}
105+	}()
106+
107+	exit := func() {
108+		logger.Info("stopping ssh server")
109+		cancel()
110+	}
111+
112+	<-done
113+	exit()
114+}
R db/db.go => pkg/db/db.go
+0, -0
R db/postgres/storage.go => pkg/db/postgres/storage.go
+1, -1
1@@ -13,7 +13,7 @@ import (
2 	"slices"
3 
4 	_ "github.com/lib/pq"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 )
9 
R db/stub/stub.go => pkg/db/stub/stub.go
+1, -1
1@@ -6,7 +6,7 @@ import (
2 	"log/slog"
3 	"time"
4 
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 )
8 
9 type StubDB struct {
R db/util.go => pkg/db/util.go
+0, -0
R filehandlers/imgs/handler.go => pkg/filehandlers/imgs/handler.go
+21, -22
 1@@ -11,15 +11,14 @@ import (
 2 	"slices"
 3 	"strings"
 4 
 5-	"github.com/charmbracelet/ssh"
 6 	exifremove "github.com/neurosnap/go-exif-remove"
 7-	"github.com/picosh/pico/db"
 8-	"github.com/picosh/pico/shared"
 9-	"github.com/picosh/pico/shared/storage"
10-	"github.com/picosh/pico/wish"
11-	"github.com/picosh/pobj"
12-	sst "github.com/picosh/pobj/storage"
13-	sendutils "github.com/picosh/send/utils"
14+	"github.com/picosh/pico/pkg/db"
15+	"github.com/picosh/pico/pkg/pobj"
16+	sst "github.com/picosh/pico/pkg/pobj/storage"
17+	"github.com/picosh/pico/pkg/pssh"
18+	sendutils "github.com/picosh/pico/pkg/send/utils"
19+	"github.com/picosh/pico/pkg/shared"
20+	"github.com/picosh/pico/pkg/shared/storage"
21 	"github.com/picosh/utils"
22 )
23 
24@@ -53,11 +52,11 @@ func (h *UploadImgHandler) getObjectPath(fpath string) string {
25 	return filepath.Join("prose", fpath)
26 }
27 
28-func (h *UploadImgHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
29+func (h *UploadImgHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
30 	var fileList []os.FileInfo
31 
32-	logger := wish.GetLogger(s)
33-	user := wish.GetUser(s)
34+	logger := pssh.GetLogger(s)
35+	user := pssh.GetUser(s)
36 
37 	if user == nil {
38 		err := fmt.Errorf("could not get user from ctx")
39@@ -102,9 +101,9 @@ func (h *UploadImgHandler) List(s ssh.Session, fpath string, isDir bool, recursi
40 	return fileList, nil
41 }
42 
43-func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
44-	logger := wish.GetLogger(s)
45-	user := wish.GetUser(s)
46+func (h *UploadImgHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
47+	logger := pssh.GetLogger(s)
48+	user := pssh.GetUser(s)
49 
50 	if user == nil {
51 		err := fmt.Errorf("could not get user from ctx")
52@@ -139,9 +138,9 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.F
53 	return fileInfo, reader, nil
54 }
55 
56-func (h *UploadImgHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
57-	logger := wish.GetLogger(s)
58-	user := wish.GetUser(s)
59+func (h *UploadImgHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
60+	logger := pssh.GetLogger(s)
61+	user := pssh.GetUser(s)
62 
63 	if user == nil {
64 		err := fmt.Errorf("could not get user from ctx")
65@@ -222,9 +221,9 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (str
66 	return str, nil
67 }
68 
69-func (h *UploadImgHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
70-	logger := wish.GetLogger(s)
71-	user := wish.GetUser(s)
72+func (h *UploadImgHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
73+	logger := pssh.GetLogger(s)
74+	user := pssh.GetUser(s)
75 
76 	if user == nil {
77 		err := fmt.Errorf("could not get user from ctx")
78@@ -309,13 +308,13 @@ func (h *UploadImgHandler) metaImg(data *PostMetaData) error {
79 	return nil
80 }
81 
82-func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
83+func (h *UploadImgHandler) writeImg(s *pssh.SSHServerConnSession, data *PostMetaData) error {
84 	valid, err := h.validateImg(data)
85 	if !valid {
86 		return err
87 	}
88 
89-	logger := wish.GetLogger(s)
90+	logger := pssh.GetLogger(s)
91 	logger = logger.With(
92 		"filename", data.Filename,
93 	)
R filehandlers/post_handler.go => pkg/filehandlers/post_handler.go
+16, -17
 1@@ -10,11 +10,10 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/shared"
 8-	"github.com/picosh/pico/wish"
 9-	sendutils "github.com/picosh/send/utils"
10+	"github.com/picosh/pico/pkg/db"
11+	"github.com/picosh/pico/pkg/pssh"
12+	sendutils "github.com/picosh/pico/pkg/send/utils"
13+	"github.com/picosh/pico/pkg/shared"
14 	"github.com/picosh/utils"
15 )
16 
17@@ -28,8 +27,8 @@ type PostMetaData struct {
18 }
19 
20 type ScpFileHooks interface {
21-	FileValidate(s ssh.Session, data *PostMetaData) (bool, error)
22-	FileMeta(s ssh.Session, data *PostMetaData) error
23+	FileValidate(s *pssh.SSHServerConnSession, data *PostMetaData) (bool, error)
24+	FileMeta(s *pssh.SSHServerConnSession, data *PostMetaData) error
25 }
26 
27 type ScpUploadHandler struct {
28@@ -46,13 +45,13 @@ func NewScpPostHandler(dbpool db.DB, cfg *shared.ConfigSite, hooks ScpFileHooks)
29 	}
30 }
31 
32-func (r *ScpUploadHandler) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
33+func (r *ScpUploadHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
34 	return BaseList(s, fpath, isDir, recursive, []string{r.Cfg.Space}, r.DBPool)
35 }
36 
37-func (h *ScpUploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
38-	logger := wish.GetLogger(s)
39-	user := wish.GetUser(s)
40+func (h *ScpUploadHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReadAndReaderAtCloser, error) {
41+	logger := pssh.GetLogger(s)
42+	user := pssh.GetUser(s)
43 
44 	if user == nil {
45 		err := fmt.Errorf("could not get user from ctx")
46@@ -83,9 +82,9 @@ func (h *ScpUploadHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.F
47 	return fileInfo, reader, nil
48 }
49 
50-func (h *ScpUploadHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
51-	logger := wish.GetLogger(s)
52-	user := wish.GetUser(s)
53+func (h *ScpUploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) (string, error) {
54+	logger := pssh.GetLogger(s)
55+	user := pssh.GetUser(s)
56 
57 	if user == nil {
58 		err := fmt.Errorf("could not get user from ctx")
59@@ -271,9 +270,9 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (str
60 	return h.Cfg.FullPostURL(curl, user.Name, metadata.Slug), nil
61 }
62 
63-func (h *ScpUploadHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) error {
64-	logger := wish.GetLogger(s)
65-	user := wish.GetUser(s)
66+func (h *ScpUploadHandler) Delete(s *pssh.SSHServerConnSession, entry *sendutils.FileEntry) error {
67+	logger := pssh.GetLogger(s)
68+	user := pssh.GetUser(s)
69 
70 	if user == nil {
71 		err := fmt.Errorf("could not get user from ctx")
R filehandlers/router_handler.go => pkg/filehandlers/router_handler.go
+20, -21
 1@@ -8,18 +8,17 @@ import (
 2 	"os"
 3 	"path/filepath"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/shared"
 8-	"github.com/picosh/pico/wish"
 9-	"github.com/picosh/send/utils"
10+	"github.com/picosh/pico/pkg/db"
11+	"github.com/picosh/pico/pkg/pssh"
12+	"github.com/picosh/pico/pkg/send/utils"
13+	"github.com/picosh/pico/pkg/shared"
14 )
15 
16 type ReadWriteHandler interface {
17-	List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error)
18-	Write(ssh.Session, *utils.FileEntry) (string, error)
19-	Read(ssh.Session, *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error)
20-	Delete(ssh.Session, *utils.FileEntry) error
21+	List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error)
22+	Write(*pssh.SSHServerConnSession, *utils.FileEntry) (string, error)
23+	Read(*pssh.SSHServerConnSession, *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error)
24+	Delete(*pssh.SSHServerConnSession, *utils.FileEntry) error
25 }
26 
27 type FileHandlerRouter struct {
28@@ -54,7 +53,7 @@ func (r *FileHandlerRouter) findHandler(fp string) (ReadWriteHandler, error) {
29 	return handler, nil
30 }
31 
32-func (r *FileHandlerRouter) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
33+func (r *FileHandlerRouter) Write(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
34 	if entry.Mode.IsDir() {
35 		return "", os.ErrInvalid
36 	}
37@@ -66,7 +65,7 @@ func (r *FileHandlerRouter) Write(s ssh.Session, entry *utils.FileEntry) (string
38 	return handler.Write(s, entry)
39 }
40 
41-func (r *FileHandlerRouter) Delete(s ssh.Session, entry *utils.FileEntry) error {
42+func (r *FileHandlerRouter) Delete(s *pssh.SSHServerConnSession, entry *utils.FileEntry) error {
43 	handler, err := r.findHandler(entry.Filepath)
44 	if err != nil {
45 		return err
46@@ -74,7 +73,7 @@ func (r *FileHandlerRouter) Delete(s ssh.Session, entry *utils.FileEntry) error
47 	return handler.Delete(s, entry)
48 }
49 
50-func (r *FileHandlerRouter) Read(s ssh.Session, entry *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
51+func (r *FileHandlerRouter) Read(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
52 	handler, err := r.findHandler(entry.Filepath)
53 	if err != nil {
54 		return nil, nil, err
55@@ -82,7 +81,7 @@ func (r *FileHandlerRouter) Read(s ssh.Session, entry *utils.FileEntry) (os.File
56 	return handler.Read(s, entry)
57 }
58 
59-func (r *FileHandlerRouter) List(s ssh.Session, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
60+func (r *FileHandlerRouter) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
61 	files := []os.FileInfo{}
62 	for key, handler := range r.FileMap {
63 		// TODO: hack because we have duplicate keys for .md and .css
64@@ -100,13 +99,13 @@ func (r *FileHandlerRouter) List(s ssh.Session, fpath string, isDir bool, recurs
65 	return files, nil
66 }
67 
68-func (r *FileHandlerRouter) GetLogger(s ssh.Session) *slog.Logger {
69-	return wish.GetLogger(s)
70+func (r *FileHandlerRouter) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
71+	return pssh.GetLogger(s)
72 }
73 
74-func (r *FileHandlerRouter) Validate(s ssh.Session) error {
75-	logger := wish.GetLogger(s)
76-	user := wish.GetUser(s)
77+func (r *FileHandlerRouter) Validate(s *pssh.SSHServerConnSession) error {
78+	logger := pssh.GetLogger(s)
79+	user := pssh.GetUser(s)
80 
81 	if user == nil {
82 		err := fmt.Errorf("could not get user from ctx")
83@@ -122,10 +121,10 @@ func (r *FileHandlerRouter) Validate(s ssh.Session) error {
84 	return nil
85 }
86 
87-func BaseList(s ssh.Session, fpath string, isDir bool, recursive bool, spaces []string, dbpool db.DB) ([]os.FileInfo, error) {
88+func BaseList(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool, spaces []string, dbpool db.DB) ([]os.FileInfo, error) {
89 	var fileList []os.FileInfo
90-	logger := wish.GetLogger(s)
91-	user := wish.GetUser(s)
92+	logger := pssh.GetLogger(s)
93+	user := pssh.GetUser(s)
94 
95 	var err error
96 
A pkg/pobj/asset.go
+45, -0
 1@@ -0,0 +1,45 @@
 2+package pobj
 3+
 4+import (
 5+	"fmt"
 6+
 7+	"github.com/picosh/pico/pkg/pssh"
 8+	"github.com/picosh/pico/pkg/send/utils"
 9+)
10+
11+type AssetNames interface {
12+	BucketName(sesh *pssh.SSHServerConnSession) (string, error)
13+	ObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error)
14+	PrintObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry, bucketName string) (string, error)
15+}
16+
17+type AssetNamesBasic struct{}
18+
19+var _ AssetNames = &AssetNamesBasic{}
20+var _ AssetNames = (*AssetNamesBasic)(nil)
21+
22+func (an *AssetNamesBasic) BucketName(sesh *pssh.SSHServerConnSession) (string, error) {
23+	return sesh.User(), nil
24+}
25+func (an *AssetNamesBasic) ObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
26+	return entry.Filepath, nil
27+}
28+func (an *AssetNamesBasic) PrintObjectName(sesh *pssh.SSHServerConnSession, entry *utils.FileEntry, bucketName string) (string, error) {
29+	objectName, err := an.ObjectName(sesh, entry)
30+	if err != nil {
31+		return "", err
32+	}
33+	return fmt.Sprintf("%s%s", bucketName, objectName), nil
34+}
35+
36+type AssetNamesForceBucket struct {
37+	*AssetNamesBasic
38+	Name string
39+}
40+
41+var _ AssetNames = &AssetNamesForceBucket{}
42+var _ AssetNames = (*AssetNamesForceBucket)(nil)
43+
44+func (an *AssetNamesForceBucket) BucketName(sesh *pssh.SSHServerConnSession) (string, error) {
45+	return an.Name, nil
46+}
A pkg/pobj/handler.go
+269, -0
  1@@ -0,0 +1,269 @@
  2+package pobj
  3+
  4+import (
  5+	"bytes"
  6+	"encoding/binary"
  7+	"fmt"
  8+	"io"
  9+	"log/slog"
 10+	"os"
 11+	"path/filepath"
 12+	"time"
 13+
 14+	"github.com/picosh/pico/pkg/pobj/storage"
 15+	"github.com/picosh/pico/pkg/pssh"
 16+	"github.com/picosh/pico/pkg/send/utils"
 17+)
 18+
 19+type ctxBucketKey struct{}
 20+
 21+func getBucket(ctx *pssh.SSHServerConnSession) (storage.Bucket, error) {
 22+	bucket, ok := ctx.Value(ctxBucketKey{}).(storage.Bucket)
 23+	if !ok {
 24+		return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
 25+	}
 26+	if bucket.Name == "" {
 27+		return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
 28+	}
 29+	return bucket, nil
 30+}
 31+func setBucket(ctx *pssh.SSHServerConnSession, bucket storage.Bucket) {
 32+	ctx.SetValue(ctxBucketKey{}, bucket)
 33+}
 34+
 35+type FileData struct {
 36+	*utils.FileEntry
 37+	Text   []byte
 38+	User   string
 39+	Bucket storage.Bucket
 40+}
 41+
 42+type Config struct {
 43+	Logger     *slog.Logger
 44+	Storage    storage.ObjectStorage
 45+	AssetNames AssetNames
 46+}
 47+
 48+type UploadAssetHandler struct {
 49+	Cfg *Config
 50+}
 51+
 52+var _ utils.CopyFromClientHandler = &UploadAssetHandler{}
 53+var _ utils.CopyFromClientHandler = (*UploadAssetHandler)(nil)
 54+
 55+func NewUploadAssetHandler(cfg *Config) *UploadAssetHandler {
 56+	if cfg.AssetNames == nil {
 57+		cfg.AssetNames = &AssetNamesBasic{}
 58+	}
 59+
 60+	return &UploadAssetHandler{
 61+		Cfg: cfg,
 62+	}
 63+}
 64+
 65+func (h *UploadAssetHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
 66+	return h.Cfg.Logger
 67+}
 68+
 69+func (h *UploadAssetHandler) Delete(s *pssh.SSHServerConnSession, entry *utils.FileEntry) error {
 70+	h.Cfg.Logger.Info("deleting file", "file", entry.Filepath)
 71+	bucket, err := getBucket(s)
 72+	if err != nil {
 73+		h.Cfg.Logger.Error(err.Error())
 74+		return err
 75+	}
 76+
 77+	objectFileName, err := h.Cfg.AssetNames.ObjectName(s, entry)
 78+	if err != nil {
 79+		return err
 80+	}
 81+	return h.Cfg.Storage.DeleteObject(bucket, objectFileName)
 82+}
 83+
 84+func (h *UploadAssetHandler) Read(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (os.FileInfo, utils.ReadAndReaderAtCloser, error) {
 85+	fileInfo := &utils.VirtualFile{
 86+		FName:    filepath.Base(entry.Filepath),
 87+		FIsDir:   false,
 88+		FSize:    entry.Size,
 89+		FModTime: time.Unix(entry.Mtime, 0),
 90+	}
 91+	h.Cfg.Logger.Info("reading file", "file", fileInfo)
 92+
 93+	bucketName, err := h.Cfg.AssetNames.BucketName(s)
 94+	if err != nil {
 95+		return nil, nil, err
 96+	}
 97+	bucket, err := h.Cfg.Storage.GetBucket(bucketName)
 98+	if err != nil {
 99+		return nil, nil, err
100+	}
101+
102+	fname, err := h.Cfg.AssetNames.ObjectName(s, entry)
103+	if err != nil {
104+		return nil, nil, err
105+	}
106+	contents, info, err := h.Cfg.Storage.GetObject(bucket, fname)
107+	if err != nil {
108+		return nil, nil, err
109+	}
110+
111+	fileInfo.FSize = info.Size
112+	fileInfo.FModTime = info.LastModified
113+
114+	reader := NewAllReaderAt(contents)
115+
116+	return fileInfo, reader, nil
117+}
118+
119+func (h *UploadAssetHandler) List(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive bool) ([]os.FileInfo, error) {
120+	h.Cfg.Logger.Info(
121+		"listing path",
122+		"dir", fpath,
123+		"isDir", isDir,
124+		"recursive", recursive,
125+	)
126+	var fileList []os.FileInfo
127+
128+	cleanFilename := fpath
129+
130+	bucketName, err := h.Cfg.AssetNames.BucketName(s)
131+	if err != nil {
132+		return fileList, err
133+	}
134+	bucket, err := h.Cfg.Storage.GetBucket(bucketName)
135+	if err != nil {
136+		return fileList, err
137+	}
138+
139+	fname, err := h.Cfg.AssetNames.ObjectName(s, &utils.FileEntry{Filepath: cleanFilename})
140+	if err != nil {
141+		return fileList, err
142+	}
143+
144+	if fname == "" || fname == "." {
145+		name := fname
146+		if name == "" {
147+			name = "/"
148+		}
149+
150+		info := &utils.VirtualFile{
151+			FName:  name,
152+			FIsDir: true,
153+		}
154+
155+		fileList = append(fileList, info)
156+	} else {
157+		name := fname
158+		if name != "/" && isDir {
159+			name += "/"
160+		}
161+
162+		foundList, err := h.Cfg.Storage.ListObjects(bucket, name, recursive)
163+		if err != nil {
164+			return fileList, err
165+		}
166+
167+		fileList = append(fileList, foundList...)
168+	}
169+
170+	return fileList, nil
171+}
172+
173+func (h *UploadAssetHandler) Validate(s *pssh.SSHServerConnSession) error {
174+	var err error
175+	userName := s.User()
176+
177+	assetBucket, err := h.Cfg.AssetNames.BucketName(s)
178+	if err != nil {
179+		return err
180+	}
181+	bucket, err := h.Cfg.Storage.UpsertBucket(assetBucket)
182+	if err != nil {
183+		return err
184+	}
185+	setBucket(s, bucket)
186+
187+	pk, _ := utils.KeyText(s)
188+	h.Cfg.Logger.Info(
189+		"attempting to upload files",
190+		"user", userName,
191+		"bucket", bucket.Name,
192+		"publicKey", pk,
193+	)
194+	return nil
195+}
196+
197+func (h *UploadAssetHandler) Write(s *pssh.SSHServerConnSession, entry *utils.FileEntry) (string, error) {
198+	var origText []byte
199+	if b, err := io.ReadAll(entry.Reader); err == nil {
200+		origText = b
201+	}
202+	fileSize := binary.Size(origText)
203+	// TODO: hack for now until I figure out how to get correct
204+	// filesize from sftp,scp,rsync
205+	entry.Size = int64(fileSize)
206+	userName := s.User()
207+
208+	bucket, err := getBucket(s)
209+	if err != nil {
210+		h.Cfg.Logger.Error(err.Error())
211+		return "", err
212+	}
213+
214+	data := &FileData{
215+		FileEntry: entry,
216+		User:      userName,
217+		Text:      origText,
218+		Bucket:    bucket,
219+	}
220+	err = h.writeAsset(s, data)
221+	if err != nil {
222+		h.Cfg.Logger.Error(err.Error())
223+		return "", err
224+	}
225+
226+	url, err := h.Cfg.AssetNames.PrintObjectName(s, entry, bucket.Name)
227+	if err != nil {
228+		return "", err
229+	}
230+	return url, nil
231+}
232+
233+func (h *UploadAssetHandler) validateAsset(_ *FileData) (bool, error) {
234+	return true, nil
235+}
236+
237+func (h *UploadAssetHandler) writeAsset(s *pssh.SSHServerConnSession, data *FileData) error {
238+	valid, err := h.validateAsset(data)
239+	if !valid {
240+		return err
241+	}
242+
243+	objectFileName, err := h.Cfg.AssetNames.ObjectName(s, data.FileEntry)
244+	if err != nil {
245+		return err
246+	}
247+	reader := bytes.NewReader(data.Text)
248+
249+	h.Cfg.Logger.Info(
250+		"uploading file to bucket",
251+		"user",
252+		data.User,
253+		"bucket",
254+		data.Bucket.Name,
255+		"object",
256+		objectFileName,
257+	)
258+
259+	_, _, err = h.Cfg.Storage.PutObject(
260+		data.Bucket,
261+		objectFileName,
262+		utils.NopReadAndReaderAtCloser(reader),
263+		data.FileEntry,
264+	)
265+	if err != nil {
266+		return err
267+	}
268+
269+	return nil
270+}
A pkg/pobj/reader.go
+42, -0
 1@@ -0,0 +1,42 @@
 2+package pobj
 3+
 4+import (
 5+	"errors"
 6+	"io"
 7+	"net/http"
 8+
 9+	"github.com/minio/minio-go/v7"
10+	"github.com/picosh/pico/pkg/send/utils"
11+)
12+
13+type AllReaderAt struct {
14+	Reader utils.ReadAndReaderAtCloser
15+}
16+
17+func NewAllReaderAt(reader utils.ReadAndReaderAtCloser) *AllReaderAt {
18+	return &AllReaderAt{reader}
19+}
20+
21+func (a *AllReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
22+	n, err = a.Reader.ReadAt(p, off)
23+
24+	if errors.Is(err, io.EOF) {
25+		return
26+	}
27+
28+	resp := minio.ToErrorResponse(err)
29+
30+	if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
31+		err = io.EOF
32+	}
33+
34+	return
35+}
36+
37+func (a *AllReaderAt) Read(p []byte) (int, error) {
38+	return a.Reader.Read(p)
39+}
40+
41+func (a *AllReaderAt) Close() error {
42+	return a.Reader.Close()
43+}
A pkg/pobj/storage/fs.go
+218, -0
  1@@ -0,0 +1,218 @@
  2+package storage
  3+
  4+import (
  5+	"fmt"
  6+	"io"
  7+	"io/fs"
  8+	"log/slog"
  9+	"os"
 10+	"path"
 11+	"path/filepath"
 12+	"strings"
 13+	"time"
 14+
 15+	"github.com/picosh/pico/pkg/send/utils"
 16+)
 17+
 18+// https://stackoverflow.com/a/32482941
 19+func dirSize(path string) (int64, error) {
 20+	var size int64
 21+	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
 22+		if err != nil {
 23+			return err
 24+		}
 25+		if !info.IsDir() {
 26+			size += info.Size()
 27+		}
 28+		return err
 29+	})
 30+
 31+	return size, err
 32+}
 33+
 34+type StorageFS struct {
 35+	Dir    string
 36+	Logger *slog.Logger
 37+}
 38+
 39+var _ ObjectStorage = &StorageFS{}
 40+var _ ObjectStorage = (*StorageFS)(nil)
 41+
 42+func NewStorageFS(logger *slog.Logger, dir string) (*StorageFS, error) {
 43+	return &StorageFS{Logger: logger, Dir: dir}, nil
 44+}
 45+
 46+func (s *StorageFS) GetBucket(name string) (Bucket, error) {
 47+	dirPath := filepath.Join(s.Dir, name)
 48+	bucket := Bucket{
 49+		Name: name,
 50+		Path: dirPath,
 51+	}
 52+	s.Logger.Info("get bucket", "dir", dirPath)
 53+
 54+	info, err := os.Stat(dirPath)
 55+	if os.IsNotExist(err) {
 56+		return bucket, fmt.Errorf("directory does not exist: %v %w", dirPath, err)
 57+	}
 58+
 59+	if err != nil {
 60+		return bucket, fmt.Errorf("directory error: %v %w", dirPath, err)
 61+
 62+	}
 63+
 64+	if !info.IsDir() {
 65+		return bucket, fmt.Errorf("directory is a file, not a directory: %#v", dirPath)
 66+	}
 67+
 68+	return bucket, nil
 69+}
 70+
 71+func (s *StorageFS) UpsertBucket(name string) (Bucket, error) {
 72+	s.Logger.Info("upsert bucket", "name", name)
 73+	bucket, err := s.GetBucket(name)
 74+	if err == nil {
 75+		return bucket, nil
 76+	}
 77+
 78+	dir := filepath.Join(s.Dir, bucket.Path)
 79+	s.Logger.Info("bucket not found, creating", "dir", dir, "err", err)
 80+	err = os.MkdirAll(dir, os.ModePerm)
 81+	if err != nil {
 82+		return bucket, err
 83+	}
 84+
 85+	return bucket, nil
 86+}
 87+
 88+func (s *StorageFS) GetBucketQuota(bucket Bucket) (uint64, error) {
 89+	dsize, err := dirSize(bucket.Path)
 90+	return uint64(dsize), err
 91+}
 92+
 93+func (s *StorageFS) DeleteBucket(bucket Bucket) error {
 94+	return os.RemoveAll(bucket.Path)
 95+}
 96+
 97+func (s *StorageFS) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
 98+	objInfo := &ObjectInfo{
 99+		LastModified: time.Time{},
100+		Metadata:     nil,
101+		UserMetadata: map[string]string{},
102+	}
103+
104+	dat, err := os.Open(filepath.Join(bucket.Path, fpath))
105+	if err != nil {
106+		return nil, objInfo, err
107+	}
108+
109+	info, err := dat.Stat()
110+	if err != nil {
111+		return nil, objInfo, err
112+	}
113+
114+	objInfo.Size = info.Size()
115+	objInfo.LastModified = info.ModTime()
116+	return dat, objInfo, nil
117+}
118+
119+func (s *StorageFS) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
120+	loc := filepath.Join(bucket.Path, fpath)
121+	err := os.MkdirAll(filepath.Dir(loc), os.ModePerm)
122+	if err != nil {
123+		return "", 0, err
124+	}
125+	f, err := os.OpenFile(loc, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
126+	if err != nil {
127+		return "", 0, err
128+	}
129+
130+	size, err := io.Copy(f, contents)
131+	if err != nil {
132+		return "", 0, err
133+	}
134+
135+	f.Close()
136+
137+	if entry.Mtime > 0 {
138+		uTime := time.Unix(entry.Mtime, 0)
139+		_ = os.Chtimes(loc, uTime, uTime)
140+	}
141+
142+	return loc, size, nil
143+}
144+
145+func (s *StorageFS) DeleteObject(bucket Bucket, fpath string) error {
146+	loc := filepath.Join(bucket.Path, fpath)
147+	err := os.Remove(loc)
148+	if err != nil {
149+		return err
150+	}
151+
152+	return nil
153+}
154+
155+func (s *StorageFS) ListBuckets() ([]string, error) {
156+	return []string{}, fmt.Errorf("not implemented")
157+}
158+
159+func (s *StorageFS) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
160+	var fileList []os.FileInfo
161+
162+	fpath := path.Join(bucket.Path, dir)
163+
164+	info, err := os.Stat(fpath)
165+	if err != nil {
166+		return fileList, err
167+	}
168+
169+	if info.IsDir() && !strings.HasSuffix(dir, "/") {
170+		fileList = append(fileList, &utils.VirtualFile{
171+			FName:    "",
172+			FIsDir:   info.IsDir(),
173+			FSize:    info.Size(),
174+			FModTime: info.ModTime(),
175+		})
176+
177+		return fileList, err
178+	}
179+
180+	var files []fs.DirEntry
181+
182+	if recursive {
183+		err = filepath.WalkDir(fpath, func(s string, d fs.DirEntry, err error) error {
184+			if err != nil {
185+				return err
186+			}
187+			files = append(files, d)
188+			return nil
189+		})
190+		if err != nil {
191+			fileList = append(fileList, info)
192+			return fileList, nil
193+		}
194+	} else {
195+		files, err = os.ReadDir(fpath)
196+		if err != nil {
197+			fileList = append(fileList, info)
198+			return fileList, nil
199+		}
200+	}
201+
202+	for _, f := range files {
203+		info, err := f.Info()
204+		if err != nil {
205+			return fileList, err
206+		}
207+
208+		i := &utils.VirtualFile{
209+			FName:    f.Name(),
210+			FIsDir:   f.IsDir(),
211+			FSize:    info.Size(),
212+			FModTime: info.ModTime(),
213+		}
214+
215+		fileList = append(fileList, i)
216+	}
217+
218+	return fileList, err
219+}
A pkg/pobj/storage/memory.go
+208, -0
  1@@ -0,0 +1,208 @@
  2+package storage
  3+
  4+import (
  5+	"fmt"
  6+	"io"
  7+	"os"
  8+	"path/filepath"
  9+	"strings"
 10+	"sync"
 11+	"time"
 12+
 13+	"github.com/picosh/pico/pkg/send/utils"
 14+)
 15+
 16+type StorageMemory struct {
 17+	storage map[string]map[string]string
 18+	mu      sync.RWMutex
 19+}
 20+
 21+var _ ObjectStorage = &StorageMemory{}
 22+var _ ObjectStorage = (*StorageMemory)(nil)
 23+
 24+func NewStorageMemory(st map[string]map[string]string) (*StorageMemory, error) {
 25+	return &StorageMemory{
 26+		storage: st,
 27+	}, nil
 28+}
 29+
 30+func (s *StorageMemory) GetBucket(name string) (Bucket, error) {
 31+	s.mu.RLock()
 32+	defer s.mu.RUnlock()
 33+
 34+	bucket := Bucket{
 35+		Name: name,
 36+		Path: name,
 37+	}
 38+
 39+	_, ok := s.storage[name]
 40+	if !ok {
 41+		return bucket, fmt.Errorf("bucket does not exist")
 42+	}
 43+
 44+	return bucket, nil
 45+}
 46+
 47+func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) {
 48+	bucket, err := s.GetBucket(name)
 49+	if err == nil {
 50+		return bucket, nil
 51+	}
 52+
 53+	s.mu.Lock()
 54+	defer s.mu.Unlock()
 55+
 56+	s.storage[name] = map[string]string{}
 57+	return bucket, nil
 58+}
 59+
 60+func (s *StorageMemory) GetBucketQuota(bucket Bucket) (uint64, error) {
 61+	s.mu.RLock()
 62+	defer s.mu.RUnlock()
 63+
 64+	objects := s.storage[bucket.Path]
 65+	size := 0
 66+	for _, val := range objects {
 67+		size += len([]byte(val))
 68+	}
 69+	return uint64(size), nil
 70+}
 71+
 72+func (s *StorageMemory) DeleteBucket(bucket Bucket) error {
 73+	s.mu.Lock()
 74+	defer s.mu.Unlock()
 75+
 76+	delete(s.storage, bucket.Path)
 77+	return nil
 78+}
 79+
 80+func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
 81+	s.mu.RLock()
 82+	defer s.mu.RUnlock()
 83+
 84+	if !strings.HasPrefix(fpath, "/") {
 85+		fpath = "/" + fpath
 86+	}
 87+
 88+	objInfo := &ObjectInfo{
 89+		LastModified: time.Time{},
 90+		Metadata:     nil,
 91+		UserMetadata: map[string]string{},
 92+	}
 93+
 94+	dat, ok := s.storage[bucket.Path][fpath]
 95+	if !ok {
 96+		return nil, objInfo, fmt.Errorf("object does not exist: %s", fpath)
 97+	}
 98+
 99+	objInfo.Size = int64(len([]byte(dat)))
100+	reader := utils.NopReadAndReaderAtCloser(strings.NewReader(dat))
101+	return reader, objInfo, nil
102+}
103+
104+func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
105+	s.mu.Lock()
106+	defer s.mu.Unlock()
107+
108+	d, err := io.ReadAll(contents)
109+	if err != nil {
110+		return "", 0, err
111+	}
112+
113+	s.storage[bucket.Path][fpath] = string(d)
114+	return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil
115+}
116+
117+func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error {
118+	s.mu.Lock()
119+	defer s.mu.Unlock()
120+
121+	delete(s.storage[bucket.Path], fpath)
122+	return nil
123+}
124+
125+func (s *StorageMemory) ListBuckets() ([]string, error) {
126+	s.mu.RLock()
127+	defer s.mu.RUnlock()
128+
129+	buckets := []string{}
130+	for key := range s.storage {
131+		buckets = append(buckets, key)
132+	}
133+	return buckets, nil
134+}
135+
136+func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
137+	s.mu.RLock()
138+	defer s.mu.RUnlock()
139+
140+	var fileList []os.FileInfo
141+
142+	resolved := dir
143+
144+	if !strings.HasPrefix(resolved, "/") {
145+		resolved = "/" + resolved
146+	}
147+
148+	objects := s.storage[bucket.Path]
149+	// dir is actually an object
150+	oval, ok := objects[resolved]
151+	if ok {
152+		fileList = append(fileList, &utils.VirtualFile{
153+			FName:    filepath.Base(resolved),
154+			FIsDir:   false,
155+			FSize:    int64(len([]byte(oval))),
156+			FModTime: time.Time{},
157+		})
158+		return fileList, nil
159+	}
160+
161+	for key, val := range objects {
162+		if !strings.HasPrefix(key, resolved) {
163+			continue
164+		}
165+
166+		rep := strings.Replace(key, resolved, "", 1)
167+		fdir := filepath.Dir(rep)
168+		fname := filepath.Base(rep)
169+		paths := strings.Split(fdir, "/")
170+
171+		if fdir == "/" {
172+			ffname := filepath.Base(resolved)
173+			fileList = append(fileList, &utils.VirtualFile{
174+				FName:  ffname,
175+				FIsDir: true,
176+			})
177+		}
178+
179+		for _, p := range paths {
180+			if p == "" || p == "/" || p == "." {
181+				continue
182+			}
183+			fileList = append(fileList, &utils.VirtualFile{
184+				FName:  p,
185+				FIsDir: true,
186+			})
187+		}
188+
189+		trimRes := strings.TrimSuffix(resolved, "/")
190+		dirKey := filepath.Dir(key)
191+		if recursive {
192+			fileList = append(fileList, &utils.VirtualFile{
193+				FName:    fname,
194+				FIsDir:   false,
195+				FSize:    int64(len([]byte(val))),
196+				FModTime: time.Time{},
197+			})
198+		} else if resolved == dirKey || trimRes == dirKey {
199+			fileList = append(fileList, &utils.VirtualFile{
200+				FName:    fname,
201+				FIsDir:   false,
202+				FSize:    int64(len([]byte(val))),
203+				FModTime: time.Time{},
204+			})
205+		}
206+	}
207+
208+	return fileList, nil
209+}
A pkg/pobj/storage/minio.go
+216, -0
  1@@ -0,0 +1,216 @@
  2+package storage
  3+
  4+import (
  5+	"context"
  6+	"errors"
  7+	"fmt"
  8+	"io"
  9+	"net/url"
 10+	"os"
 11+	"strconv"
 12+	"strings"
 13+	"time"
 14+
 15+	"github.com/minio/madmin-go/v3"
 16+	"github.com/minio/minio-go/v7"
 17+	"github.com/minio/minio-go/v7/pkg/credentials"
 18+	"github.com/picosh/pico/pkg/send/utils"
 19+)
 20+
 21+type StorageMinio struct {
 22+	Client *minio.Client
 23+	Admin  *madmin.AdminClient
 24+}
 25+
 26+var _ ObjectStorage = &StorageMinio{}
 27+var _ ObjectStorage = (*StorageMinio)(nil)
 28+
 29+func NewStorageMinio(address, user, pass string) (*StorageMinio, error) {
 30+	endpoint, err := url.Parse(address)
 31+	if err != nil {
 32+		return nil, err
 33+	}
 34+	ssl := endpoint.Scheme == "https"
 35+
 36+	mClient, err := minio.New(endpoint.Host, &minio.Options{
 37+		Creds:  credentials.NewStaticV4(user, pass, ""),
 38+		Secure: ssl,
 39+	})
 40+	if err != nil {
 41+		return nil, err
 42+	}
 43+
 44+	aClient, err := madmin.NewWithOptions(
 45+		endpoint.Host,
 46+		&madmin.Options{
 47+			Creds:  credentials.NewStaticV4(user, pass, ""),
 48+			Secure: ssl,
 49+		},
 50+	)
 51+	if err != nil {
 52+		return nil, err
 53+	}
 54+
 55+	mini := &StorageMinio{
 56+		Client: mClient,
 57+		Admin:  aClient,
 58+	}
 59+	return mini, err
 60+}
 61+
 62+func (s *StorageMinio) GetBucket(name string) (Bucket, error) {
 63+	bucket := Bucket{
 64+		Name: name,
 65+	}
 66+
 67+	exists, err := s.Client.BucketExists(context.TODO(), bucket.Name)
 68+	if err != nil || !exists {
 69+		if err == nil {
 70+			err = errors.New("bucket does not exist")
 71+		}
 72+		return bucket, err
 73+	}
 74+
 75+	return bucket, nil
 76+}
 77+
 78+func (s *StorageMinio) UpsertBucket(name string) (Bucket, error) {
 79+	bucket, err := s.GetBucket(name)
 80+	if err == nil {
 81+		return bucket, nil
 82+	}
 83+
 84+	err = s.Client.MakeBucket(context.TODO(), name, minio.MakeBucketOptions{})
 85+	if err != nil {
 86+		return bucket, err
 87+	}
 88+
 89+	return bucket, nil
 90+}
 91+
 92+func (s *StorageMinio) GetBucketQuota(bucket Bucket) (uint64, error) {
 93+	info, err := s.Admin.AccountInfo(context.TODO(), madmin.AccountOpts{})
 94+	if err != nil {
 95+		return 0, nil
 96+	}
 97+	for _, b := range info.Buckets {
 98+		if b.Name == bucket.Name {
 99+			return b.Size, nil
100+		}
101+	}
102+
103+	return 0, fmt.Errorf("%s bucket not found in account info", bucket.Name)
104+}
105+
106+func (s *StorageMinio) ListBuckets() ([]string, error) {
107+	bcks := []string{}
108+	buckets, err := s.Client.ListBuckets(context.Background())
109+	if err != nil {
110+		return bcks, err
111+	}
112+	for _, bucket := range buckets {
113+		bcks = append(bcks, bucket.Name)
114+	}
115+
116+	return bcks, nil
117+}
118+
119+func (s *StorageMinio) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
120+	var fileList []os.FileInfo
121+
122+	resolved := strings.TrimPrefix(dir, "/")
123+
124+	opts := minio.ListObjectsOptions{Prefix: resolved, Recursive: recursive, WithMetadata: true}
125+	for obj := range s.Client.ListObjects(context.Background(), bucket.Name, opts) {
126+		if obj.Err != nil {
127+			return fileList, obj.Err
128+		}
129+
130+		isDir := strings.HasSuffix(obj.Key, string(os.PathSeparator))
131+
132+		modTime := obj.LastModified
133+
134+		if mtime, ok := obj.UserMetadata["Mtime"]; ok {
135+			mtimeUnix, err := strconv.Atoi(mtime)
136+			if err == nil {
137+				modTime = time.Unix(int64(mtimeUnix), 0)
138+			}
139+		}
140+
141+		info := &utils.VirtualFile{
142+			FName:    strings.TrimSuffix(strings.TrimPrefix(obj.Key, resolved), "/"),
143+			FIsDir:   isDir,
144+			FSize:    obj.Size,
145+			FModTime: modTime,
146+		}
147+		fileList = append(fileList, info)
148+	}
149+
150+	return fileList, nil
151+}
152+
153+func (s *StorageMinio) DeleteBucket(bucket Bucket) error {
154+	return s.Client.RemoveBucket(context.TODO(), bucket.Name)
155+}
156+
157+func (s *StorageMinio) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
158+	objInfo := &ObjectInfo{
159+		Size:         0,
160+		LastModified: time.Time{},
161+		ETag:         "",
162+	}
163+
164+	info, err := s.Client.StatObject(context.Background(), bucket.Name, fpath, minio.StatObjectOptions{})
165+	if err != nil {
166+		return nil, objInfo, err
167+	}
168+
169+	objInfo.LastModified = info.LastModified
170+	objInfo.ETag = info.ETag
171+	objInfo.Metadata = info.Metadata
172+	objInfo.UserMetadata = info.UserMetadata
173+	objInfo.Size = info.Size
174+
175+	obj, err := s.Client.GetObject(context.Background(), bucket.Name, fpath, minio.GetObjectOptions{})
176+	if err != nil {
177+		return nil, objInfo, err
178+	}
179+
180+	if mtime, ok := info.UserMetadata["Mtime"]; ok {
181+		mtimeUnix, err := strconv.Atoi(mtime)
182+		if err == nil {
183+			objInfo.LastModified = time.Unix(int64(mtimeUnix), 0)
184+		}
185+	}
186+
187+	return obj, objInfo, nil
188+}
189+
190+func (s *StorageMinio) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
191+	opts := minio.PutObjectOptions{
192+		UserMetadata: map[string]string{
193+			"Mtime": fmt.Sprint(time.Now().Unix()),
194+		},
195+	}
196+
197+	if entry.Mtime > 0 {
198+		opts.UserMetadata["Mtime"] = fmt.Sprint(entry.Mtime)
199+	}
200+
201+	var objSize int64 = -1
202+	if entry.Size > 0 {
203+		objSize = entry.Size
204+	}
205+	info, err := s.Client.PutObject(context.TODO(), bucket.Name, fpath, contents, objSize, opts)
206+
207+	if err != nil {
208+		return "", 0, err
209+	}
210+
211+	return fmt.Sprintf("%s/%s", info.Bucket, info.Key), info.Size, nil
212+}
213+
214+func (s *StorageMinio) DeleteObject(bucket Bucket, fpath string) error {
215+	err := s.Client.RemoveObject(context.TODO(), bucket.Name, fpath, minio.RemoveObjectOptions{})
216+	return err
217+}
A pkg/pobj/storage/storage.go
+37, -0
 1@@ -0,0 +1,37 @@
 2+package storage
 3+
 4+import (
 5+	"io"
 6+	"net/http"
 7+	"os"
 8+	"time"
 9+
10+	"github.com/picosh/pico/pkg/send/utils"
11+)
12+
13+type Bucket struct {
14+	Name string
15+	Path string
16+	Root string
17+}
18+
19+type ObjectStorage interface {
20+	GetBucket(name string) (Bucket, error)
21+	GetBucketQuota(bucket Bucket) (uint64, error)
22+	UpsertBucket(name string) (Bucket, error)
23+	ListBuckets() ([]string, error)
24+	DeleteBucket(bucket Bucket) error
25+
26+	GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error)
27+	PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error)
28+	DeleteObject(bucket Bucket, fpath string) error
29+	ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error)
30+}
31+
32+type ObjectInfo struct {
33+	Size         int64
34+	LastModified time.Time
35+	ETag         string
36+	Metadata     http.Header
37+	UserMetadata map[string]string
38+}
A pkg/pobj/util.go
+39, -0
 1@@ -0,0 +1,39 @@
 2+package pobj
 3+
 4+import (
 5+	"log/slog"
 6+	"os"
 7+
 8+	"github.com/picosh/pico/pkg/pobj/storage"
 9+)
10+
11+func GetEnv(key string, defaultVal string) string {
12+	if value, exists := os.LookupEnv(key); exists {
13+		return value
14+	}
15+	return defaultVal
16+}
17+
18+func EnvDriverDetector(logger *slog.Logger) (storage.ObjectStorage, error) {
19+	driver := GetEnv("OBJECT_DRIVER", "fs")
20+	logger.Info("driver detected", "driver", driver)
21+
22+	if driver == "memory" {
23+		return storage.NewStorageMemory(map[string]map[string]string{})
24+	} else if driver == "minio" {
25+		url := GetEnv("MINIO_URL", "")
26+		user := GetEnv("MINIO_ROOT_USER", "")
27+		pass := GetEnv("MINIO_ROOT_PASSWORD", "")
28+		logger.Info(
29+			"object config detected",
30+			"url", url,
31+			"user", user,
32+		)
33+		return storage.NewStorageMinio(url, user, pass)
34+	}
35+
36+	// implied driver == "fs"
37+	storageDir := GetEnv("OBJECT_URL", "./.storage")
38+	logger.Info("object config detected", "dir", storageDir)
39+	return storage.NewStorageFS(logger, storageDir)
40+}
A pkg/pssh/logger.go
+111, -0
  1@@ -0,0 +1,111 @@
  2+package pssh
  3+
  4+import (
  5+	"log/slog"
  6+	"time"
  7+
  8+	"github.com/picosh/pico/pkg/db"
  9+)
 10+
 11+type ctxLoggerKey struct{}
 12+type ctxUserKey struct{}
 13+
 14+type FindUserInterface interface {
 15+	FindUserByPubkey(string) (*db.User, error)
 16+}
 17+
 18+type GetLoggerInterface interface {
 19+	GetLogger(s *SSHServerConnSession) *slog.Logger
 20+}
 21+
 22+func LogMiddleware(getLogger GetLoggerInterface, db FindUserInterface) SSHServerMiddleware {
 23+	return func(sshHandler SSHServerHandler) SSHServerHandler {
 24+		return func(s *SSHServerConnSession) error {
 25+			ct := time.Now()
 26+
 27+			logger := GetLogger(s)
 28+			if logger == slog.Default() {
 29+				logger = getLogger.GetLogger(s)
 30+
 31+				user := GetUser(s)
 32+				if user == nil {
 33+					user, err := db.FindUserByPubkey(s.Permissions().Extensions["pubkey"])
 34+					if err == nil && user != nil {
 35+						logger = logger.With(
 36+							"user", user.Name,
 37+							"userId", user.ID,
 38+							"ip", s.RemoteAddr().String(),
 39+						)
 40+						s.SetValue(ctxUserKey{}, user)
 41+					}
 42+				}
 43+
 44+				s.SetValue(ctxLoggerKey{}, logger)
 45+			}
 46+
 47+			pty, _, ok := s.Pty()
 48+
 49+			width, height := 0, 0
 50+			term := ""
 51+			if pty != nil {
 52+				term = pty.Term
 53+				width = pty.Window.Width
 54+				height = pty.Window.Height
 55+			}
 56+
 57+			logger.Info(
 58+				"connect",
 59+				"sshUser", s.User(),
 60+				"pty", ok,
 61+				"term", term,
 62+				"windowWidth", width,
 63+				"windowHeight", height,
 64+			)
 65+
 66+			err := sshHandler(s)
 67+			if err != nil {
 68+				logger.Error("error", "err", err)
 69+			}
 70+
 71+			if pty != nil {
 72+				term = pty.Term
 73+				width = pty.Window.Width
 74+				height = pty.Window.Height
 75+			}
 76+
 77+			logger.Info(
 78+				"disconnect",
 79+				"sshUser", s.User(),
 80+				"pty", ok,
 81+				"term", term,
 82+				"windowWidth", width,
 83+				"windowHeight", height,
 84+				"duration", time.Since(ct),
 85+				"err", err,
 86+			)
 87+
 88+			return err
 89+		}
 90+	}
 91+}
 92+
 93+func GetLogger(s *SSHServerConnSession) *slog.Logger {
 94+	logger := slog.Default()
 95+	if s == nil {
 96+		return logger
 97+	}
 98+
 99+	if v, ok := s.Context().Value(ctxLoggerKey{}).(*slog.Logger); ok {
100+		return v
101+	}
102+
103+	return logger
104+}
105+
106+func GetUser(s *SSHServerConnSession) *db.User {
107+	if v, ok := s.Context().Value(ctxUserKey{}).(*db.User); ok {
108+		return v
109+	}
110+
111+	return nil
112+}
A pkg/pssh/pty.go
+122, -0
  1@@ -0,0 +1,122 @@
  2+package pssh
  3+
  4+import (
  5+	"encoding/binary"
  6+	"fmt"
  7+)
  8+
  9+func SessionMessage(sesh *SSHServerConnSession, msg string) {
 10+	_, _ = sesh.Write([]byte(msg + "\r\n"))
 11+}
 12+
 13+func DeprecatedNotice() SSHServerMiddleware {
 14+	return func(next SSHServerHandler) SSHServerHandler {
 15+		return func(sesh *SSHServerConnSession) error {
 16+			msg := fmt.Sprintf(
 17+				"%s\r\n\r\nRun %s to access pico's TUI",
 18+				"DEPRECATED",
 19+				"ssh pico.sh",
 20+			)
 21+			SessionMessage(sesh, msg)
 22+			return next(sesh)
 23+		}
 24+	}
 25+}
 26+
 27+func PtyMdw(mdw SSHServerMiddleware) SSHServerMiddleware {
 28+	return func(next SSHServerHandler) SSHServerHandler {
 29+		return func(sesh *SSHServerConnSession) error {
 30+			_, _, ok := sesh.Pty()
 31+			if !ok {
 32+				return next(sesh)
 33+			}
 34+			return mdw(next)(sesh)
 35+		}
 36+	}
 37+}
 38+
 39+type Window struct {
 40+	Width        int
 41+	Height       int
 42+	HeightPixels int
 43+	WidthPixels  int
 44+}
 45+
 46+type Pty struct {
 47+	Term   string
 48+	Window Window
 49+}
 50+
 51+func (p *Pty) Resize(width, height int) error {
 52+	return nil
 53+}
 54+
 55+func (p *Pty) Name() string {
 56+	return ""
 57+}
 58+
 59+func parsePtyRequest(s []byte) (pty Pty, ok bool) {
 60+	term, s, ok := parseString(s)
 61+	if !ok {
 62+		return
 63+	}
 64+	width32, s, ok := parseUint32(s)
 65+	if !ok {
 66+		return
 67+	}
 68+	height32, _, ok := parseUint32(s)
 69+	if !ok {
 70+		return
 71+	}
 72+	pty = Pty{
 73+		Term: term,
 74+		Window: Window{
 75+			Width:  int(width32),
 76+			Height: int(height32),
 77+		},
 78+	}
 79+	return
 80+}
 81+
 82+func parseWinchRequest(s []byte) (win Window, ok bool) {
 83+	width32, s, ok := parseUint32(s)
 84+	if width32 < 1 {
 85+		ok = false
 86+	}
 87+	if !ok {
 88+		return
 89+	}
 90+	height32, _, ok := parseUint32(s)
 91+	if height32 < 1 {
 92+		ok = false
 93+	}
 94+	if !ok {
 95+		return
 96+	}
 97+	win = Window{
 98+		Width:  int(width32),
 99+		Height: int(height32),
100+	}
101+	return
102+}
103+
104+func parseString(in []byte) (out string, rest []byte, ok bool) {
105+	if len(in) < 4 {
106+		return
107+	}
108+	length := binary.BigEndian.Uint32(in)
109+	if uint32(len(in)) < 4+length {
110+		return
111+	}
112+	out = string(in[4 : 4+length])
113+	rest = in[4+length:]
114+	ok = true
115+	return
116+}
117+
118+func parseUint32(in []byte) (uint32, []byte, bool) {
119+	if len(in) < 4 {
120+		return 0, nil, false
121+	}
122+	return binary.BigEndian.Uint32(in), in[4:], true
123+}
A pkg/pssh/server.go
+702, -0
  1@@ -0,0 +1,702 @@
  2+package pssh
  3+
  4+import (
  5+	"context"
  6+	"crypto/ed25519"
  7+	"crypto/rand"
  8+	"crypto/subtle"
  9+	"encoding/pem"
 10+	"errors"
 11+	"fmt"
 12+	"io"
 13+	"log/slog"
 14+	"net"
 15+	"net/http"
 16+	"os"
 17+	"strings"
 18+	"sync"
 19+	"time"
 20+
 21+	"github.com/antoniomika/syncmap"
 22+	"github.com/prometheus/client_golang/prometheus"
 23+	"github.com/prometheus/client_golang/prometheus/promauto"
 24+	"github.com/prometheus/client_golang/prometheus/promhttp"
 25+	"golang.org/x/crypto/ssh"
 26+)
 27+
 28+type SSHServerConn struct {
 29+	Ctx        context.Context
 30+	CancelFunc context.CancelFunc
 31+	Logger     *slog.Logger
 32+	Conn       *ssh.ServerConn
 33+	SSHServer  *SSHServer
 34+	Start      time.Time
 35+
 36+	mu sync.Mutex
 37+}
 38+
 39+func (s *SSHServerConn) Context() context.Context {
 40+	s.mu.Lock()
 41+	defer s.mu.Unlock()
 42+
 43+	return s.Ctx
 44+}
 45+
 46+func (sc *SSHServerConn) Close() error {
 47+	sc.CancelFunc()
 48+	return nil
 49+}
 50+
 51+type SSHServerConnSession struct {
 52+	ssh.Channel
 53+	*SSHServerConn
 54+
 55+	Ctx        context.Context
 56+	CancelFunc context.CancelFunc
 57+
 58+	pty   *Pty
 59+	winch chan Window
 60+
 61+	mu sync.Mutex
 62+}
 63+
 64+// Deadline implements context.Context.
 65+func (s *SSHServerConnSession) Deadline() (deadline time.Time, ok bool) {
 66+	s.mu.Lock()
 67+	defer s.mu.Unlock()
 68+
 69+	return s.Ctx.Deadline()
 70+}
 71+
 72+// Done implements context.Context.
 73+func (s *SSHServerConnSession) Done() <-chan struct{} {
 74+	s.mu.Lock()
 75+	defer s.mu.Unlock()
 76+
 77+	return s.Ctx.Done()
 78+}
 79+
 80+// Err implements context.Context.
 81+func (s *SSHServerConnSession) Err() error {
 82+	s.mu.Lock()
 83+	defer s.mu.Unlock()
 84+
 85+	return s.Ctx.Err()
 86+}
 87+
 88+// Value implements context.Context.
 89+func (s *SSHServerConnSession) Value(key any) any {
 90+	s.mu.Lock()
 91+	defer s.mu.Unlock()
 92+
 93+	return s.Ctx.Value(key)
 94+}
 95+
 96+// SetValue implements context.Context.
 97+func (s *SSHServerConnSession) SetValue(key any, data any) {
 98+	s.mu.Lock()
 99+	defer s.mu.Unlock()
100+
101+	s.Ctx = context.WithValue(s.Ctx, key, data)
102+}
103+
104+func (s *SSHServerConnSession) Context() context.Context {
105+	s.mu.Lock()
106+	defer s.mu.Unlock()
107+
108+	return s.Ctx
109+}
110+
111+func (s *SSHServerConnSession) Permissions() *ssh.Permissions {
112+	return s.Conn.Permissions
113+}
114+
115+func (s *SSHServerConnSession) User() string {
116+	return s.Conn.User()
117+}
118+
119+func (s *SSHServerConnSession) PublicKey() ssh.PublicKey {
120+	key, ok := s.Conn.Permissions.Extensions["pubkey"]
121+	if !ok {
122+		return nil
123+	}
124+
125+	pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
126+	if err != nil {
127+		return nil
128+	}
129+	return pk
130+}
131+
132+func (s *SSHServerConnSession) RemoteAddr() net.Addr {
133+	return s.Conn.RemoteAddr()
134+}
135+
136+func (s *SSHServerConnSession) Command() []string {
137+	cmd, _ := s.Value("command").([]string)
138+	return cmd
139+}
140+
141+func (s *SSHServerConnSession) Close() error {
142+	s.CancelFunc()
143+	return s.Channel.Close()
144+}
145+
146+func (s *SSHServerConnSession) Exit(code int) error {
147+	status := struct{ Status uint32 }{uint32(code)}
148+	_, err := s.Channel.SendRequest("exit-status", false, ssh.Marshal(&status))
149+	return err
150+}
151+
152+func (s *SSHServerConnSession) Fatal(err error) {
153+	fmt.Fprintln(s.Stderr(), err)
154+	fmt.Fprintf(s.Stderr(), "\r")
155+	_ = s.Exit(1)
156+	_ = s.Close()
157+}
158+
159+func (s *SSHServerConnSession) Pty() (*Pty, <-chan Window, bool) {
160+	s.mu.Lock()
161+	defer s.mu.Unlock()
162+
163+	if s.pty == nil {
164+		return nil, nil, false
165+	}
166+
167+	return s.pty, s.winch, true
168+}
169+
170+var _ context.Context = &SSHServerConnSession{}
171+
172+func (sc *SSHServerConn) Handle(chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) error {
173+	defer sc.Close()
174+
175+	for {
176+		select {
177+		case <-sc.Context().Done():
178+			return nil
179+		case newChan, ok := <-chans:
180+			if !ok {
181+				return nil
182+			}
183+
184+			sc.Logger.Info("new channel", "type", newChan.ChannelType(), "extraData", newChan.ExtraData())
185+			chanFunc, ok := sc.SSHServer.Config.ChannelMiddleware[newChan.ChannelType()]
186+			if !ok {
187+				sc.Logger.Info("no channel middleware for type", "type", newChan.ChannelType())
188+				continue
189+			}
190+
191+			go func() {
192+				err := chanFunc(newChan, sc)
193+				if err != nil {
194+					sc.Logger.Error("channel middleware", "err", err)
195+				}
196+			}()
197+		case req, ok := <-reqs:
198+			if !ok {
199+				return nil
200+			}
201+			sc.Logger.Info("new request", "type", req.Type, "wantReply", req.WantReply, "payload", req.Payload)
202+		}
203+	}
204+}
205+
206+func NewSSHServerConn(
207+	ctx context.Context,
208+	logger *slog.Logger,
209+	conn *ssh.ServerConn,
210+	server *SSHServer,
211+) *SSHServerConn {
212+	if ctx == nil {
213+		ctx = context.Background()
214+	}
215+
216+	cancelCtx, cancelFunc := context.WithCancel(ctx)
217+
218+	if logger == nil {
219+		logger = slog.Default()
220+	}
221+
222+	return &SSHServerConn{
223+		Ctx:        cancelCtx,
224+		CancelFunc: cancelFunc,
225+		Logger:     logger,
226+		Conn:       conn,
227+		SSHServer:  server,
228+		Start:      time.Now(),
229+	}
230+}
231+
232+type SSHServerHandler func(*SSHServerConnSession) error
233+type SSHServerMiddleware func(SSHServerHandler) SSHServerHandler
234+type SSHServerChannelMiddleware func(ssh.NewChannel, *SSHServerConn) error
235+
236+type SSHServerConfig struct {
237+	*ssh.ServerConfig
238+	App                 string
239+	ListenAddr          string
240+	PromListenAddr      string
241+	Middleware          []SSHServerMiddleware
242+	SubsystemMiddleware []SSHServerMiddleware
243+	ChannelMiddleware   map[string]SSHServerChannelMiddleware
244+}
245+
246+type SSHServer struct {
247+	Ctx        context.Context
248+	CancelFunc context.CancelFunc
249+	Logger     *slog.Logger
250+	Config     *SSHServerConfig
251+	Listener   net.Listener
252+	Conns      *syncmap.Map[string, *SSHServerConn]
253+
254+	SessionsCreated  *prometheus.CounterVec
255+	SessionsFinished *prometheus.CounterVec
256+	SessionsDuration *prometheus.CounterVec
257+}
258+
259+func (s *SSHServer) ListenAndServe() error {
260+	if s.Config.PromListenAddr != "" {
261+		s.SessionsCreated = promauto.With(prometheus.DefaultRegisterer).NewCounterVec(prometheus.CounterOpts{
262+			Name: "pssh_sessions_created_total",
263+			Help: "The total number of sessions created",
264+			ConstLabels: prometheus.Labels{
265+				"app": s.Config.App,
266+			},
267+		}, []string{"command"})
268+
269+		s.SessionsFinished = promauto.With(prometheus.DefaultRegisterer).NewCounterVec(prometheus.CounterOpts{
270+			Name: "pssh_sessions_finished_total",
271+			Help: "The total number of sessions created",
272+			ConstLabels: prometheus.Labels{
273+				"app": s.Config.App,
274+			},
275+		}, []string{"command"})
276+
277+		s.SessionsDuration = promauto.With(prometheus.DefaultRegisterer).NewCounterVec(prometheus.CounterOpts{
278+			Name: "pssh_sessions_duration_seconds",
279+			Help: "The total sessions duration in seconds",
280+			ConstLabels: prometheus.Labels{
281+				"app": s.Config.App,
282+			},
283+		}, []string{"command"})
284+
285+		go func() {
286+			mux := http.NewServeMux()
287+			mux.Handle("/metrics", promhttp.Handler())
288+
289+			srv := &http.Server{Addr: s.Config.PromListenAddr, Handler: mux}
290+
291+			go func() {
292+				<-s.Ctx.Done()
293+				s.Logger.Info("Prometheus server shutting down")
294+				srv.Close()
295+			}()
296+
297+			s.Logger.Info("Starting Prometheus server", "addr", s.Config.PromListenAddr)
298+
299+			err := srv.ListenAndServe()
300+			if err != nil {
301+				if errors.Is(err, http.ErrServerClosed) {
302+					s.Logger.Info("Prometheus server shut down")
303+					return
304+				}
305+
306+				s.Logger.Error("Prometheus serve error", "err", err)
307+				panic(err)
308+			}
309+		}()
310+	}
311+
312+	listen, err := net.Listen("tcp", s.Config.ListenAddr)
313+	if err != nil {
314+		return err
315+	}
316+
317+	s.Listener = listen
318+	defer s.Listener.Close()
319+
320+	go func() {
321+		<-s.Ctx.Done()
322+		s.Close()
323+	}()
324+
325+	var retErr error
326+
327+	for {
328+		conn, err := s.Listener.Accept()
329+		if err != nil {
330+			s.Logger.Error("accept", "err", err)
331+			if errors.Is(err, net.ErrClosed) {
332+				retErr = err
333+				break
334+			}
335+			continue
336+		}
337+
338+		go func() {
339+			if err := s.HandleConn(conn); err != nil && !errors.Is(err, io.EOF) {
340+				s.Logger.Error("Error handling connection", "err", err, "remoteAddr", conn.RemoteAddr().String())
341+			}
342+		}()
343+	}
344+
345+	if errors.Is(retErr, net.ErrClosed) {
346+		return nil
347+	}
348+
349+	return retErr
350+}
351+
352+func (s *SSHServer) HandleConn(conn net.Conn) error {
353+	defer conn.Close()
354+
355+	sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.Config.ServerConfig)
356+	if err != nil {
357+		return err
358+	}
359+
360+	newLogger := s.Logger.With(
361+		"remoteAddr", conn.RemoteAddr().String(),
362+		"user", sshConn.User(),
363+		"pubkey", sshConn.Permissions.Extensions["pubkey"],
364+	)
365+
366+	newConn := NewSSHServerConn(
367+		s.Ctx,
368+		newLogger,
369+		sshConn,
370+		s,
371+	)
372+
373+	s.Conns.Store(sshConn.RemoteAddr().String(), newConn)
374+
375+	err = newConn.Handle(chans, reqs)
376+
377+	s.Conns.Delete(sshConn.RemoteAddr().String())
378+
379+	return err
380+}
381+
382+func (s *SSHServer) Close() error {
383+	s.CancelFunc()
384+	return s.Listener.Close()
385+}
386+
387+func NewSSHServer(ctx context.Context, logger *slog.Logger, config *SSHServerConfig) *SSHServer {
388+	if ctx == nil {
389+		ctx = context.Background()
390+	}
391+
392+	cancelCtx, cancelFunc := context.WithCancel(ctx)
393+
394+	if logger == nil {
395+		logger = slog.Default()
396+	}
397+
398+	if config == nil {
399+		config = &SSHServerConfig{}
400+	}
401+
402+	if config.ChannelMiddleware == nil {
403+		config.ChannelMiddleware = map[string]SSHServerChannelMiddleware{}
404+	}
405+
406+	if _, ok := config.ChannelMiddleware["session"]; !ok {
407+		config.ChannelMiddleware["session"] = func(newChan ssh.NewChannel, sc *SSHServerConn) error {
408+			channel, requests, err := newChan.Accept()
409+			if err != nil {
410+				sc.Logger.Error("accept session channel", "err", err)
411+				return err
412+			}
413+
414+			ctx, cancelFunc := context.WithCancel(sc.Ctx)
415+
416+			sesh := &SSHServerConnSession{
417+				Channel:       channel,
418+				SSHServerConn: sc,
419+				Ctx:           ctx,
420+				CancelFunc:    cancelFunc,
421+			}
422+
423+			for {
424+				select {
425+				case <-sesh.Done():
426+					return nil
427+				case req, ok := <-requests:
428+					if !ok {
429+						return nil
430+					}
431+
432+					go func() {
433+						sc.Logger.Info("new session request", "type", req.Type, "wantReply", req.WantReply, "payload", req.Payload)
434+						switch req.Type {
435+						case "subsystem":
436+							if len(sc.SSHServer.Config.SubsystemMiddleware) == 0 {
437+								err := req.Reply(false, nil)
438+								if err != nil {
439+									sc.Logger.Error("subsystem reply", "err", err)
440+								}
441+
442+								err = sc.Close()
443+								if err != nil {
444+									sc.Logger.Error("subsystem close", "err", err)
445+								}
446+
447+								sesh.Fatal(err)
448+								return
449+							}
450+
451+							h := func(*SSHServerConnSession) error { return nil }
452+							for _, m := range sc.SSHServer.Config.SubsystemMiddleware {
453+								h = m(h)
454+							}
455+
456+							err := req.Reply(true, nil)
457+							if err != nil {
458+								sc.Logger.Error("subsystem reply", "err", err)
459+								sesh.Fatal(err)
460+								return
461+							}
462+
463+							if err := h(sesh); err != nil && !errors.Is(err, io.EOF) {
464+								sc.Logger.Error("subsystem middleware", "err", err)
465+								sesh.Fatal(err)
466+								return
467+							}
468+
469+							err = sesh.Exit(0)
470+							if err != nil {
471+								sc.Logger.Error("subsystem exit", "err", err)
472+							}
473+
474+							err = sesh.Close()
475+							if err != nil {
476+								sc.Logger.Error("subsystem close", "err", err)
477+							}
478+						case "shell", "exec":
479+							if len(sc.SSHServer.Config.Middleware) == 0 {
480+								err := req.Reply(false, nil)
481+								if err != nil {
482+									sc.Logger.Error("shell/exec reply", "err", err)
483+								}
484+								sesh.Fatal(err)
485+								return
486+							}
487+
488+							if len(req.Payload) > 0 {
489+								var payload = struct{ Value string }{}
490+								err := ssh.Unmarshal(req.Payload, &payload)
491+								if err != nil {
492+									sc.Logger.Error("shell/exec unmarshal", "err", err)
493+									sesh.Fatal(err)
494+									return
495+								}
496+
497+								if sc.SSHServer.Config.PromListenAddr != "" {
498+									sc.SSHServer.SessionsCreated.WithLabelValues(payload.Value).Inc()
499+									defer func() {
500+										sc.SSHServer.SessionsFinished.WithLabelValues(payload.Value).Inc()
501+										sc.SSHServer.SessionsDuration.WithLabelValues(payload.Value).Add(time.Since(sc.Start).Seconds())
502+									}()
503+								}
504+
505+								sesh.SetValue("command", strings.Fields(payload.Value))
506+							}
507+
508+							h := func(*SSHServerConnSession) error { return nil }
509+							for _, m := range sc.SSHServer.Config.Middleware {
510+								h = m(h)
511+							}
512+
513+							err = req.Reply(true, nil)
514+							if err != nil {
515+								sc.Logger.Error("shell/exec reply", "err", err)
516+								sesh.Fatal(err)
517+								return
518+							}
519+
520+							if err := h(sesh); err != nil && !errors.Is(err, io.EOF) {
521+								sc.Logger.Error("exec middleware", "err", err)
522+								sesh.Fatal(err)
523+								return
524+							}
525+
526+							err = sesh.Exit(0)
527+							if err != nil {
528+								sc.Logger.Error("subsystem exit", "err", err)
529+							}
530+
531+							err = sesh.Close()
532+							if err != nil {
533+								sc.Logger.Error("subsystem close", "err", err)
534+							}
535+						case "pty-req":
536+							sesh.mu.Lock()
537+							found := sesh.pty != nil
538+							sesh.mu.Unlock()
539+							if found {
540+								err := req.Reply(false, nil)
541+								if err != nil {
542+									sc.Logger.Error("pty-req reply", "err", err)
543+								}
544+								return
545+							}
546+
547+							ptyReq, ok := parsePtyRequest(req.Payload)
548+							if !ok {
549+								err := req.Reply(false, nil)
550+								if err != nil {
551+									sc.Logger.Error("pty-req reply", "err", err)
552+								}
553+								return
554+							}
555+
556+							sesh.mu.Lock()
557+							sesh.pty = &ptyReq
558+							sesh.winch = make(chan Window, 1)
559+							sesh.mu.Unlock()
560+
561+							sesh.winch <- ptyReq.Window
562+							err := req.Reply(ok, nil)
563+							if err != nil {
564+								sc.Logger.Error("pty-req reply", "err", err)
565+							}
566+						case "window-change":
567+							sesh.mu.Lock()
568+							found := sesh.pty != nil
569+							sesh.mu.Unlock()
570+
571+							if !found {
572+								err := req.Reply(false, nil)
573+								if err != nil {
574+									sc.Logger.Error("pty-req reply", "err", err)
575+								}
576+								return
577+							}
578+
579+							win, ok := parseWinchRequest(req.Payload)
580+							if ok {
581+								sesh.mu.Lock()
582+								sesh.pty.Window = win
583+								sesh.winch <- win
584+								sesh.mu.Unlock()
585+							}
586+
587+							err := req.Reply(ok, nil)
588+							if err != nil {
589+								sc.Logger.Error("window-change reply", "err", err)
590+							}
591+						}
592+					}()
593+				}
594+			}
595+		}
596+	}
597+
598+	server := &SSHServer{
599+		Ctx:        cancelCtx,
600+		CancelFunc: cancelFunc,
601+		Logger:     logger,
602+		Config:     config,
603+		Conns:      syncmap.New[string, *SSHServerConn](),
604+	}
605+
606+	return server
607+}
608+
609+type PubKeyAuthHandler func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
610+
611+func NewSSHServerWithConfig(
612+	ctx context.Context,
613+	logger *slog.Logger,
614+	app, host, port, promPort string,
615+	pubKeyAuthHandler PubKeyAuthHandler,
616+	middleware, subsystemMiddleware []SSHServerMiddleware,
617+	channelMiddleware map[string]SSHServerChannelMiddleware) (*SSHServer, error) {
618+	server := NewSSHServer(ctx, logger, &SSHServerConfig{
619+		App:        app,
620+		ListenAddr: fmt.Sprintf("%s:%s", host, port),
621+		ServerConfig: &ssh.ServerConfig{
622+			PublicKeyCallback: pubKeyAuthHandler,
623+		},
624+		Middleware:          middleware,
625+		SubsystemMiddleware: subsystemMiddleware,
626+		ChannelMiddleware:   channelMiddleware,
627+	})
628+
629+	if promPort != "" {
630+		server.Config.PromListenAddr = fmt.Sprintf("%s:%s", host, promPort)
631+	}
632+
633+	pemBytes, err := os.ReadFile("ssh_data/term_info_ed25519")
634+	if err != nil {
635+		logger.Error("failed to read private key file", "error", err)
636+		if !os.IsNotExist(err) {
637+			return nil, err
638+		}
639+
640+		logger.Info("generating new private key")
641+
642+		pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
643+		if err != nil {
644+			logger.Error("failed to generate private key", "error", err)
645+			return nil, err
646+		}
647+
648+		privb, err := ssh.MarshalPrivateKey(privKey, "")
649+		if err != nil {
650+			logger.Error("failed to marshal private key", "error", err)
651+			return nil, err
652+		}
653+
654+		block := &pem.Block{
655+			Type:  "OPENSSH PRIVATE KEY",
656+			Bytes: privb.Bytes,
657+		}
658+
659+		if err = os.MkdirAll("ssh_data", 0700); err != nil {
660+			logger.Error("failed to create ssh_data directory", "error", err)
661+			return nil, err
662+		}
663+
664+		pemBytes = pem.EncodeToMemory(block)
665+
666+		if err = os.WriteFile("ssh_data/term_info_ed25519", pemBytes, 0600); err != nil {
667+			logger.Error("failed to write private key", "error", err)
668+			return nil, err
669+		}
670+
671+		sshPubKey, err := ssh.NewPublicKey(pubKey)
672+		if err != nil {
673+			logger.Error("failed to create public key", "error", err)
674+			return nil, err
675+		}
676+
677+		pubb := ssh.MarshalAuthorizedKey(sshPubKey)
678+		if err = os.WriteFile("ssh_data/term_info_ed25519.pub", pubb, 0600); err != nil {
679+			logger.Error("failed to write public key", "error", err)
680+			return nil, err
681+		}
682+	}
683+
684+	signer, err := ssh.ParsePrivateKey(pemBytes)
685+	if err != nil {
686+		logger.Error("failed to parse private key", "error", err)
687+		return nil, err
688+	}
689+
690+	server.Config.AddHostKey(signer)
691+
692+	return server, nil
693+}
694+
695+func KeysEqual(a, b ssh.PublicKey) bool {
696+	if a == nil || b == nil {
697+		return false
698+	}
699+
700+	am := a.Marshal()
701+	bm := b.Marshal()
702+	return (len(am) == len(bm) && subtle.ConstantTimeCompare(am, bm) == 1)
703+}
A pkg/pssh/server_test.go
+275, -0
  1@@ -0,0 +1,275 @@
  2+package pssh_test
  3+
  4+import (
  5+	"context"
  6+	"errors"
  7+	"log/slog"
  8+	"net"
  9+	"testing"
 10+	"time"
 11+
 12+	"github.com/picosh/pico/pkg/pssh"
 13+	"golang.org/x/crypto/ssh"
 14+)
 15+
 16+func TestNewSSHServer(t *testing.T) {
 17+	ctx := context.Background()
 18+	logger := slog.Default()
 19+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
 20+
 21+	if server == nil {
 22+		t.Fatal("expected non-nil server")
 23+	}
 24+
 25+	if server.Ctx == nil {
 26+		t.Error("expected non-nil context")
 27+	}
 28+
 29+	if server.Logger == nil {
 30+		t.Error("expected non-nil logger")
 31+	}
 32+
 33+	if server.Config == nil {
 34+		t.Error("expected non-nil config")
 35+	}
 36+
 37+	if server.Conns == nil {
 38+		t.Error("expected non-nil connections map")
 39+	}
 40+}
 41+
 42+func TestNewSSHServerConn(t *testing.T) {
 43+	ctx := context.Background()
 44+	logger := slog.Default()
 45+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
 46+	conn := &ssh.ServerConn{}
 47+
 48+	serverConn := pssh.NewSSHServerConn(ctx, logger, conn, server)
 49+
 50+	if serverConn == nil {
 51+		t.Fatal("expected non-nil server connection")
 52+	}
 53+
 54+	if serverConn.Ctx == nil {
 55+		t.Error("expected non-nil context")
 56+	}
 57+
 58+	if serverConn.Logger == nil {
 59+		t.Error("expected non-nil logger")
 60+	}
 61+
 62+	if serverConn.Conn != conn {
 63+		t.Error("expected conn to match")
 64+	}
 65+
 66+	if serverConn.SSHServer != server {
 67+		t.Error("expected server to match")
 68+	}
 69+}
 70+
 71+func TestSSHServerConnClose(t *testing.T) {
 72+	ctx := context.Background()
 73+	logger := slog.Default()
 74+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
 75+	conn := &ssh.ServerConn{}
 76+
 77+	serverConn := pssh.NewSSHServerConn(ctx, logger, conn, server)
 78+	err := serverConn.Close()
 79+
 80+	if err != nil {
 81+		t.Errorf("unexpected error: %v", err)
 82+	}
 83+
 84+	// Should be canceled after close
 85+	select {
 86+	case <-serverConn.Ctx.Done():
 87+		// Context was canceled as expected
 88+	default:
 89+		t.Error("context was not canceled after Close()")
 90+	}
 91+}
 92+
 93+func TestSSHServerClose(t *testing.T) {
 94+	ctx := context.Background()
 95+	logger := slog.Default()
 96+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
 97+
 98+	// Create a mock listener to test Close()
 99+	listener, err := net.Listen("tcp", "127.0.0.1:0")
100+	if err != nil {
101+		t.Fatalf("failed to create listener: %v", err)
102+	}
103+
104+	server.Listener = listener
105+	err = server.Close()
106+
107+	if err != nil {
108+		t.Errorf("unexpected error: %v", err)
109+	}
110+
111+	// Should be canceled after close
112+	select {
113+	case <-server.Ctx.Done():
114+		// Context was canceled as expected
115+	default:
116+		t.Error("context was not canceled after Close()")
117+	}
118+}
119+
120+func TestSSHServerNilParams(t *testing.T) {
121+	// Test with nil context and logger
122+	//nolint:staticcheck // SA1012 ignores nil check
123+	//lint:ignore SA1012 ignores nil check
124+	server := pssh.NewSSHServer(nil, nil, nil)
125+
126+	if server == nil {
127+		t.Fatal("expected non-nil server")
128+	}
129+
130+	if server.Ctx == nil {
131+		t.Error("expected non-nil context even when nil is passed")
132+	}
133+
134+	if server.Logger == nil {
135+		t.Error("expected non-nil logger even when nil is passed")
136+	}
137+
138+	// Test with nil context and logger for connection
139+	//nolint:staticcheck // SA1012 ignores nil check
140+	//lint:ignore SA1012 ignores nil check
141+	conn := pssh.NewSSHServerConn(nil, nil, &ssh.ServerConn{}, server)
142+
143+	if conn == nil {
144+		t.Fatal("expected non-nil server connection")
145+	}
146+
147+	if conn.Ctx == nil {
148+		t.Error("expected non-nil context even when nil is passed")
149+	}
150+
151+	if conn.Logger == nil {
152+		t.Error("expected non-nil logger even when nil is passed")
153+	}
154+}
155+
156+func TestSSHServerHandleConn(t *testing.T) {
157+	ctx, cancel := context.WithCancel(context.Background())
158+	defer cancel()
159+	logger := slog.Default()
160+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
161+
162+	// Setup a basic SSH server config
163+	config := &ssh.ServerConfig{
164+		NoClientAuth: true,
165+	}
166+
167+	server.Config.ServerConfig = config
168+
169+	// Create a mock connection
170+	client, server_conn := net.Pipe()
171+	defer client.Close()
172+
173+	// Start HandleConn in a goroutine
174+	errChan := make(chan error, 1)
175+	go func() {
176+		errChan <- server.HandleConn(server_conn)
177+	}()
178+
179+	// Configure SSH client
180+	clientConfig := &ssh.ClientConfig{
181+		User:            "testuser",
182+		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
183+	}
184+
185+	// Try to establish SSH connection
186+	_, _, _, err := ssh.NewClientConn(client, "", clientConfig)
187+
188+	// It should fail since we're using a pipe and not a proper SSH handshake
189+	if err == nil {
190+		t.Error("expected SSH handshake to fail with test pipe")
191+	}
192+
193+	// Close connections to ensure HandleConn returns
194+	client.Close()
195+	server_conn.Close()
196+
197+	// Wait for HandleConn to return
198+	select {
199+	case <-errChan:
200+		// Expected HandleConn to return
201+	case <-time.After(2 * time.Second):
202+		t.Error("HandleConn did not return after connection closed")
203+	}
204+}
205+
206+func TestSSHServerListenAndServe(t *testing.T) {
207+	ctx, cancel := context.WithCancel(context.Background())
208+	defer cancel()
209+	logger := slog.Default()
210+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
211+
212+	config := &ssh.ServerConfig{
213+		NoClientAuth: true,
214+	}
215+
216+	// Set a random port
217+	port := "127.0.0.1:0"
218+	server.Config.ListenAddr = port
219+	server.Config.ServerConfig = config
220+
221+	// Start server in a goroutine
222+	errChan := make(chan error, 1)
223+	go func() {
224+		err := server.ListenAndServe()
225+		errChan <- err
226+	}()
227+
228+	// Wait a bit for the server to start
229+	time.Sleep(100 * time.Millisecond)
230+
231+	// Trigger cancellation to stop the server
232+	cancel()
233+
234+	// Wait for server to stop
235+	select {
236+	case err := <-errChan:
237+		if err != nil && !errors.Is(err, net.ErrClosed) {
238+			t.Errorf("unexpected error: %v", err)
239+		}
240+	case <-time.After(2 * time.Second):
241+		t.Error("server did not shut down in time")
242+	}
243+}
244+
245+func TestSSHServerConnHandle(t *testing.T) {
246+	ctx, cancel := context.WithCancel(context.Background())
247+	defer cancel()
248+	logger := slog.Default()
249+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{})
250+	conn := &ssh.ServerConn{}
251+
252+	serverConn := pssh.NewSSHServerConn(ctx, logger, conn, server)
253+
254+	// Create channels for testing
255+	chans := make(chan ssh.NewChannel)
256+	reqs := make(chan *ssh.Request)
257+
258+	// Start handle in a goroutine
259+	errChan := make(chan error, 1)
260+	go func() {
261+		errChan <- serverConn.Handle(chans, reqs)
262+	}()
263+
264+	// Ensure handle returns when context is canceled
265+	cancel()
266+
267+	// Wait for handle to return
268+	select {
269+	case err := <-errChan:
270+		if err != nil {
271+			t.Errorf("unexpected error: %v", err)
272+		}
273+	case <-time.After(2 * time.Second):
274+		t.Error("Handle did not return after context canceled")
275+	}
276+}
A pkg/send/auth/auth.go
+26, -0
 1@@ -0,0 +1,26 @@
 2+package auth
 3+
 4+import (
 5+	"github.com/picosh/pico/pkg/pssh"
 6+	"github.com/picosh/pico/pkg/send/utils"
 7+)
 8+
 9+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
10+	return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
11+		return func(session *pssh.SSHServerConnSession) error {
12+			defer func() {
13+				if r := recover(); r != nil {
14+					writeHandler.GetLogger(session).Error("error running auth middleware", "err", r)
15+				}
16+			}()
17+
18+			err := writeHandler.Validate(session)
19+			if err != nil {
20+				utils.ErrorHandler(session, err)
21+				return err
22+			}
23+
24+			return sshHandler(session)
25+		}
26+	}
27+}
A pkg/send/list/list.go
+44, -0
 1@@ -0,0 +1,44 @@
 2+package list
 3+
 4+import (
 5+	"sort"
 6+	"strings"
 7+
 8+	"github.com/picosh/pico/pkg/pssh"
 9+	"github.com/picosh/pico/pkg/send/utils"
10+)
11+
12+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
13+	return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
14+		return func(session *pssh.SSHServerConnSession) error {
15+			cmd := session.Command()
16+			if !(len(cmd) > 1 && cmd[0] == "command" && cmd[1] == "ls") {
17+				return sshHandler(session)
18+			}
19+
20+			fileList, err := writeHandler.List(session, "/", true, false)
21+			if err != nil {
22+				utils.ErrorHandler(session, err)
23+				return err
24+			}
25+
26+			var data []string
27+			for _, file := range fileList {
28+				name := strings.ReplaceAll(file.Name(), "/", "")
29+				if file.IsDir() {
30+					name += "/"
31+				}
32+
33+				data = append(data, name)
34+			}
35+
36+			sort.Strings(data)
37+
38+			_, err = session.Write([]byte(strings.Join(data, "\r\n")))
39+			if err != nil {
40+				utils.ErrorHandler(session, err)
41+			}
42+			return err
43+		}
44+	}
45+}
A pkg/send/pipe/pipe.go
+64, -0
 1@@ -0,0 +1,64 @@
 2+package pipe
 3+
 4+import (
 5+	"fmt"
 6+	"io/fs"
 7+	"strconv"
 8+	"strings"
 9+	"time"
10+
11+	"github.com/picosh/pico/pkg/pssh"
12+	"github.com/picosh/pico/pkg/send/utils"
13+)
14+
15+func Middleware(writeHandler utils.CopyFromClientHandler, ext string) pssh.SSHServerMiddleware {
16+	return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
17+		return func(session *pssh.SSHServerConnSession) error {
18+			_, _, activePty := session.Pty()
19+			if activePty {
20+				_ = session.Exit(0)
21+				err := session.Close()
22+				return err
23+			}
24+
25+			cmd := session.Command()
26+
27+			name := ""
28+			if len(cmd) > 0 {
29+				name = strings.TrimSpace(cmd[0])
30+				if strings.Contains(name, "=") {
31+					name = ""
32+				}
33+			}
34+
35+			postTime := time.Now()
36+
37+			if name == "" {
38+				name = fmt.Sprintf("%s%s", strconv.Itoa(int(postTime.UnixNano())), ext)
39+			}
40+
41+			result, err := writeHandler.Write(session, &utils.FileEntry{
42+				Filepath: name,
43+				Mode:     fs.FileMode(0777),
44+				Size:     0,
45+				Mtime:    postTime.Unix(),
46+				Atime:    postTime.Unix(),
47+				Reader:   session,
48+			})
49+			if err != nil {
50+				utils.ErrorHandler(session, err)
51+				return err
52+			}
53+
54+			if result != "" {
55+				_, err = session.Write([]byte(fmt.Sprintf("%s\r\n", result)))
56+				if err != nil {
57+					utils.ErrorHandler(session, err)
58+				}
59+				return err
60+			}
61+
62+			return sshHandler(session)
63+		}
64+	}
65+}
A pkg/send/protocols/rsync/rsync.go
+244, -0
  1@@ -0,0 +1,244 @@
  2+package rsync
  3+
  4+import (
  5+	"errors"
  6+	"fmt"
  7+	"io/fs"
  8+	"os"
  9+	"path"
 10+	"slices"
 11+	"strings"
 12+
 13+	"github.com/picosh/go-rsync-receiver/rsyncopts"
 14+	"github.com/picosh/go-rsync-receiver/rsyncreceiver"
 15+	"github.com/picosh/go-rsync-receiver/rsyncsender"
 16+	rsyncutils "github.com/picosh/go-rsync-receiver/utils"
 17+	"github.com/picosh/pico/pkg/pssh"
 18+	"github.com/picosh/pico/pkg/send/utils"
 19+)
 20+
 21+type handler struct {
 22+	session      *pssh.SSHServerConnSession
 23+	writeHandler utils.CopyFromClientHandler
 24+	root         string
 25+	recursive    bool
 26+	ignoreTimes  bool
 27+}
 28+
 29+func (h *handler) List(rPath string) ([]fs.FileInfo, error) {
 30+	isDir := false
 31+	if rPath == "." {
 32+		rPath = "/"
 33+		isDir = true
 34+	}
 35+
 36+	list, err := h.writeHandler.List(h.session, rPath, isDir, h.recursive)
 37+	if err != nil {
 38+		return nil, err
 39+	}
 40+
 41+	var dirs []string
 42+
 43+	var newList []fs.FileInfo
 44+
 45+	for _, f := range list {
 46+		if !f.IsDir() && f.Size() == 0 {
 47+			continue
 48+		}
 49+
 50+		fname := f.Name()
 51+		if strings.HasPrefix(f.Name(), "/") {
 52+			fname = path.Join(rPath, f.Name())
 53+		}
 54+
 55+		if fname == "" && !f.IsDir() {
 56+			fname = path.Base(rPath)
 57+		}
 58+
 59+		newFile := &utils.VirtualFile{
 60+			FName:    fname,
 61+			FIsDir:   f.IsDir(),
 62+			FSize:    f.Size(),
 63+			FModTime: f.ModTime(),
 64+			FSys:     f.Sys(),
 65+		}
 66+
 67+		newList = append(newList, newFile)
 68+
 69+		parts := strings.Split(newFile.Name(), string(os.PathSeparator))
 70+		lastDir := newFile.Name()
 71+		for i := 0; i < len(parts); i++ {
 72+			lastDir, _ = path.Split(lastDir)
 73+			if lastDir == "" {
 74+				continue
 75+			}
 76+
 77+			lastDir = lastDir[:len(lastDir)-1]
 78+			dirs = append(dirs, lastDir)
 79+		}
 80+	}
 81+
 82+	for _, dir := range dirs {
 83+		newList = append(newList, &utils.VirtualFile{
 84+			FName:  dir,
 85+			FIsDir: true,
 86+		})
 87+	}
 88+
 89+	slices.Reverse(newList)
 90+
 91+	onlyEmpty := true
 92+	for _, f := range newList {
 93+		if f.Name() != "" {
 94+			onlyEmpty = false
 95+		}
 96+	}
 97+
 98+	if len(newList) == 0 || onlyEmpty {
 99+		return nil, errors.New("no files to send, the directory may not exist or could be empty")
100+	}
101+
102+	return newList, nil
103+}
104+
105+func (h *handler) Read(file *rsyncutils.SenderFile) (os.FileInfo, rsyncutils.ReaderAtCloser, error) {
106+	filePath := file.WPath
107+
108+	if strings.HasSuffix(h.root, file.WPath) {
109+		filePath = h.root
110+	} else if !strings.HasPrefix(filePath, h.root) {
111+		filePath = path.Join(h.root, file.Path, file.WPath)
112+	}
113+
114+	return h.writeHandler.Read(h.session, &utils.FileEntry{Filepath: filePath})
115+}
116+
117+func (h *handler) Put(file *rsyncutils.ReceiverFile) (int64, error) {
118+	fileEntry := &utils.FileEntry{
119+		Filepath: path.Join("/", h.root, file.Name),
120+		Mode:     fs.FileMode(0600),
121+		Size:     file.Length,
122+		Mtime:    file.ModTime.Unix(),
123+		Atime:    file.ModTime.Unix(),
124+	}
125+	fileEntry.Reader = file.Reader
126+
127+	msg, err := h.writeHandler.Write(h.session, fileEntry)
128+	if err != nil {
129+		errMsg := fmt.Sprintf("%s\r\n", err.Error())
130+		_, err = h.session.Stderr().Write([]byte(errMsg))
131+	}
132+	if msg != "" {
133+		nMsg := fmt.Sprintf("%s\r\n", msg)
134+		_, err = h.session.Stderr().Write([]byte(nMsg))
135+	}
136+	return 0, err
137+}
138+
139+func (h *handler) Remove(willReceive []*rsyncutils.ReceiverFile) error {
140+	entries, err := h.writeHandler.List(h.session, path.Join("/", h.root), true, true)
141+	if err != nil {
142+		return err
143+	}
144+
145+	var toDelete []string
146+
147+	for _, entry := range entries {
148+		exists := slices.ContainsFunc(willReceive, func(rf *rsyncutils.ReceiverFile) bool {
149+			return rf.Name == entry.Name()
150+		})
151+
152+		if !exists && entry.Name() != "._pico_keep_dir" {
153+			toDelete = append(toDelete, entry.Name())
154+		}
155+	}
156+
157+	var errs []error
158+
159+	for _, file := range toDelete {
160+		errs = append(errs, h.writeHandler.Delete(h.session, &utils.FileEntry{Filepath: path.Join("/", h.root, file)}))
161+		_, err = h.session.Stderr().Write([]byte(fmt.Sprintf("deleting %s\r\n", file)))
162+		errs = append(errs, err)
163+	}
164+
165+	return errors.Join(errs...)
166+}
167+
168+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
169+	return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
170+		return func(session *pssh.SSHServerConnSession) error {
171+			cmd := session.Command()
172+			if len(cmd) == 0 || cmd[0] != "rsync" {
173+				return sshHandler(session)
174+			}
175+
176+			logger := writeHandler.GetLogger(session).With(
177+				"rsync", true,
178+				"cmd", cmd,
179+			)
180+
181+			defer func() {
182+				if r := recover(); r != nil {
183+					logger.Error("error running rsync middleware", "err", r)
184+					_, _ = session.Stderr().Write([]byte("error running rsync middleware, check the flags you are using\r\n"))
185+				}
186+			}()
187+
188+			cmdFlags := session.Command()
189+
190+			optsCtx, err := rsyncopts.ParseArguments(cmdFlags[1:], true)
191+			if err != nil {
192+				fmt.Fprintf(session.Stderr(), "error parsing rsync arguments: %s\r\n", err.Error())
193+				return err
194+			}
195+
196+			if optsCtx.Options.Compress() {
197+				err := fmt.Errorf("compression is currently unsupported")
198+				fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
199+				return err
200+			}
201+
202+			if optsCtx.Options.AlwaysChecksum() {
203+				err := fmt.Errorf("checksum is currently unsupported")
204+				fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
205+				return err
206+			}
207+
208+			if len(optsCtx.RemainingArgs) != 2 {
209+				err := fmt.Errorf("missing source and destination arguments")
210+				fmt.Fprintf(session.Stderr(), "error: %s\r\n", err.Error())
211+				return err
212+			}
213+
214+			root := strings.TrimPrefix(optsCtx.RemainingArgs[len(optsCtx.RemainingArgs)-1], "/")
215+			if root == "" {
216+				root = "/"
217+			}
218+
219+			fileHandler := &handler{
220+				session:      session,
221+				writeHandler: writeHandler,
222+				root:         root,
223+				recursive:    optsCtx.Options.Recurse(),
224+				ignoreTimes:  !optsCtx.Options.PreserveMTimes(),
225+			}
226+
227+			for _, arg := range cmd {
228+				if arg == "--sender" {
229+					err := rsyncsender.ClientRun(logger, optsCtx.Options, session, fileHandler, []string{fileHandler.root}, true)
230+					if err != nil {
231+						logger.Error("error running rsync sender", "err", err)
232+					}
233+					return err
234+				}
235+			}
236+
237+			err = rsyncreceiver.ClientRun(logger, optsCtx.Options, session, fileHandler, []string{fileHandler.root}, true)
238+			if err != nil {
239+				logger.Error("error running rsync receiver", "err", err)
240+			}
241+
242+			return err
243+		}
244+	}
245+}
A pkg/send/protocols/scp/copy_from_client.go
+141, -0
  1@@ -0,0 +1,141 @@
  2+package scp
  3+
  4+import (
  5+	"bufio"
  6+	"errors"
  7+	"fmt"
  8+	"io"
  9+	"io/fs"
 10+	"path/filepath"
 11+	"regexp"
 12+	"strconv"
 13+
 14+	"github.com/picosh/pico/pkg/pssh"
 15+	"github.com/picosh/pico/pkg/send/utils"
 16+)
 17+
 18+var (
 19+	reTimestamp = regexp.MustCompile(`^T(\d{10}) 0 (\d{10}) 0$`)
 20+	reNewFolder = regexp.MustCompile(`^D(\d{4}) 0 (.*)$`)
 21+	reNewFile   = regexp.MustCompile(`^C(\d{4}) (\d+) (.*)$`)
 22+)
 23+
 24+type parseError struct {
 25+	subject string
 26+}
 27+
 28+func (e parseError) Error() string {
 29+	return fmt.Sprintf("failed to parse: %q", e.subject)
 30+}
 31+
 32+func copyFromClient(session *pssh.SSHServerConnSession, info Info, handler utils.CopyFromClientHandler) error {
 33+	// accepts the request
 34+	_, _ = session.Write(utils.NULL)
 35+
 36+	writeErrors := []error{}
 37+	writeSuccess := []string{}
 38+
 39+	var (
 40+		path  = info.Path
 41+		r     = bufio.NewReader(session)
 42+		mtime int64
 43+		atime int64
 44+	)
 45+
 46+	for {
 47+		line, _, err := r.ReadLine()
 48+		if err != nil {
 49+			if errors.Is(err, io.EOF) {
 50+				break
 51+			}
 52+			return fmt.Errorf("failed to read line: %w", err)
 53+		}
 54+
 55+		if matches := reTimestamp.FindAllStringSubmatch(string(line), 2); matches != nil {
 56+			mtime, err = strconv.ParseInt(matches[0][1], 10, 64)
 57+			if err != nil {
 58+				return parseError{string(line)}
 59+			}
 60+			atime, err = strconv.ParseInt(matches[0][2], 10, 64)
 61+			if err != nil {
 62+				return parseError{string(line)}
 63+			}
 64+
 65+			// accepts the header
 66+			_, _ = session.Write(utils.NULL)
 67+			continue
 68+		}
 69+
 70+		if matches := reNewFile.FindAllStringSubmatch(string(line), 3); matches != nil {
 71+			if len(matches) != 1 || len(matches[0]) != 4 {
 72+				return parseError{string(line)}
 73+			}
 74+
 75+			mode, err := strconv.ParseUint(matches[0][1], 8, 32)
 76+			if err != nil {
 77+				return parseError{string(line)}
 78+			}
 79+
 80+			size, err := strconv.ParseInt(matches[0][2], 10, 64)
 81+			if err != nil {
 82+				return parseError{string(line)}
 83+			}
 84+			name := matches[0][3]
 85+
 86+			// accepts the header
 87+			_, _ = session.Write(utils.NULL)
 88+
 89+			result, err := handler.Write(session, &utils.FileEntry{
 90+				Filepath: filepath.Join(path, name),
 91+				Mode:     fs.FileMode(mode),
 92+				Size:     size,
 93+				Mtime:    mtime,
 94+				Atime:    atime,
 95+				Reader:   utils.NewLimitReader(r, int(size)),
 96+			})
 97+
 98+			if err == nil {
 99+				writeSuccess = append(writeSuccess, result)
100+			} else {
101+				writeErrors = append(writeErrors, err)
102+				fmt.Printf("failed to write file: %q: %v\n", name, err)
103+			}
104+
105+			// read the trailing nil char
106+			_, _ = r.ReadByte() // TODO: check if it is indeed a utils.NULL?
107+
108+			mtime = 0
109+			atime = 0
110+			// says 'hey im done'
111+			_, _ = session.Write(utils.NULL)
112+			continue
113+		}
114+
115+		if matches := reNewFolder.FindAllStringSubmatch(string(line), 2); matches != nil {
116+			if len(matches) != 1 || len(matches[0]) != 3 {
117+				return parseError{string(line)}
118+			}
119+
120+			name := matches[0][2]
121+			path = filepath.Join(path, name)
122+			// says 'hey im done'
123+			_, _ = session.Write(utils.NULL)
124+			continue
125+		}
126+
127+		if string(line) == "E" {
128+			path = filepath.Dir(path)
129+
130+			// says 'hey im done'
131+			_, _ = session.Write(utils.NULL)
132+			continue
133+		}
134+
135+		return fmt.Errorf("unhandled input: %q", string(line))
136+	}
137+
138+	utils.PrintMsg(session, writeSuccess, writeErrors)
139+
140+	_, _ = session.Write(utils.NULL)
141+	return nil
142+}
A pkg/send/protocols/scp/copy_to_client.go
+12, -0
 1@@ -0,0 +1,12 @@
 2+package scp
 3+
 4+import (
 5+	"errors"
 6+
 7+	"github.com/picosh/pico/pkg/pssh"
 8+	"github.com/picosh/pico/pkg/send/utils"
 9+)
10+
11+func copyToClient(session *pssh.SSHServerConnSession, info Info, handler utils.CopyFromClientHandler) error {
12+	return errors.New("unsupported, use rsync or sftp")
13+}
A pkg/send/protocols/scp/scp.go
+107, -0
  1@@ -0,0 +1,107 @@
  2+package scp
  3+
  4+import (
  5+	"fmt"
  6+
  7+	"github.com/picosh/pico/pkg/pssh"
  8+	"github.com/picosh/pico/pkg/send/utils"
  9+)
 10+
 11+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
 12+	return func(sshHandler pssh.SSHServerHandler) pssh.SSHServerHandler {
 13+		return func(session *pssh.SSHServerConnSession) error {
 14+			cmd := session.Command()
 15+			if len(cmd) == 0 || cmd[0] != "scp" {
 16+				return sshHandler(session)
 17+			}
 18+
 19+			logger := writeHandler.GetLogger(session).With(
 20+				"scp", true,
 21+				"cmd", cmd,
 22+			)
 23+
 24+			defer func() {
 25+				if r := recover(); r != nil {
 26+					logger.Error("error running scp middleware", "err", r)
 27+					_, _ = session.Stderr().Write([]byte("error running scp middleware, check the flags you are using\r\n"))
 28+				}
 29+			}()
 30+
 31+			info := GetInfo(cmd)
 32+			if !info.Ok {
 33+				return sshHandler(session)
 34+			}
 35+
 36+			var err error
 37+
 38+			switch info.Op {
 39+			case OpCopyToClient:
 40+				if writeHandler == nil {
 41+					err = fmt.Errorf("no handler provided for scp -t")
 42+					break
 43+				}
 44+				err = copyToClient(session, info, writeHandler)
 45+			case OpCopyFromClient:
 46+				if writeHandler == nil {
 47+					err = fmt.Errorf("no handler provided for scp -t")
 48+					break
 49+				}
 50+				err = copyFromClient(session, info, writeHandler)
 51+			}
 52+			if err != nil {
 53+				utils.ErrorHandler(session, err)
 54+			}
 55+
 56+			return err
 57+		}
 58+	}
 59+}
 60+
 61+// Op defines which kind of SCP Operation is going on.
 62+type Op byte
 63+
 64+const (
 65+	// OpCopyToClient is when a file is being copied from the server to the client.
 66+	OpCopyToClient Op = 'f'
 67+
 68+	// OpCopyFromClient is when a file is being copied from the client into the server.
 69+	OpCopyFromClient Op = 't'
 70+)
 71+
 72+// Info provides some information about the current SCP Operation.
 73+type Info struct {
 74+	// Ok is true if the current session is a SCP.
 75+	Ok bool
 76+
 77+	// Recursice is true if its a recursive SCP.
 78+	Recursive bool
 79+
 80+	// Path is the server path of the scp operation.
 81+	Path string
 82+
 83+	// Op is the SCP operation kind.
 84+	Op Op
 85+}
 86+
 87+func GetInfo(cmd []string) Info {
 88+	info := Info{}
 89+	if len(cmd) == 0 || cmd[0] != "scp" {
 90+		return info
 91+	}
 92+
 93+	for i, p := range cmd {
 94+		switch p {
 95+		case "-r":
 96+			info.Recursive = true
 97+		case "-f":
 98+			info.Op = OpCopyToClient
 99+			info.Path = cmd[i+1]
100+		case "-t":
101+			info.Op = OpCopyFromClient
102+			info.Path = cmd[i+1]
103+		}
104+	}
105+
106+	info.Ok = true
107+	return info
108+}
A pkg/send/protocols/sftp/handler.go
+176, -0
  1@@ -0,0 +1,176 @@
  2+package sftp
  3+
  4+import (
  5+	"bytes"
  6+	"errors"
  7+	"fmt"
  8+	"io"
  9+	"io/fs"
 10+	"os"
 11+	"path/filepath"
 12+
 13+	"slices"
 14+
 15+	"github.com/picosh/pico/pkg/pssh"
 16+	"github.com/picosh/pico/pkg/send/utils"
 17+	"github.com/pkg/sftp"
 18+)
 19+
 20+type listerat []os.FileInfo
 21+
 22+func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
 23+	var n int
 24+	if offset >= int64(len(f)) {
 25+		return 0, io.EOF
 26+	}
 27+	n = copy(ls, f[offset:])
 28+	if n < len(ls) {
 29+		return n, io.EOF
 30+	}
 31+	return n, nil
 32+}
 33+
 34+type handler struct {
 35+	session      *pssh.SSHServerConnSession
 36+	writeHandler utils.CopyFromClientHandler
 37+}
 38+
 39+func (f *handler) Filecmd(r *sftp.Request) error {
 40+	switch r.Method {
 41+	case "Rmdir", "Remove":
 42+		entry := toFileEntry(r)
 43+
 44+		if r.Method == "Rmdir" {
 45+			entry.Mode = os.ModeDir
 46+		}
 47+
 48+		return f.writeHandler.Delete(f.session, entry)
 49+	case "Mkdir":
 50+		entry := toFileEntry(r)
 51+
 52+		entry.Mode = os.ModeDir
 53+
 54+		_, err := f.writeHandler.Write(f.session, entry)
 55+
 56+		return err
 57+	case "Setstat":
 58+		return nil
 59+	}
 60+	return errors.New("unsupported")
 61+}
 62+
 63+func (f *handler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
 64+	switch r.Method {
 65+	case "List", "Stat":
 66+		list := r.Method == "List"
 67+
 68+		listData, err := f.writeHandler.List(f.session, r.Filepath, list, false)
 69+		if err != nil {
 70+			return nil, err
 71+		}
 72+
 73+		// an empty string from minio or exact match from filepath base name is what we want
 74+
 75+		if !list {
 76+			listData = slices.DeleteFunc(listData, func(f os.FileInfo) bool {
 77+				return !(f.Name() == "" || f.Name() == filepath.Base(r.Filepath))
 78+			})
 79+		}
 80+
 81+		if r.Filepath == "/" {
 82+			listData = slices.DeleteFunc(listData, func(f os.FileInfo) bool {
 83+				return f.Name() == "/"
 84+			})
 85+			listData = slices.Insert(listData, 0, os.FileInfo(&utils.VirtualFile{
 86+				FName:  ".",
 87+				FIsDir: true,
 88+			}))
 89+		}
 90+
 91+		return listerat(listData), nil
 92+	}
 93+
 94+	return nil, errors.New("unsupported")
 95+}
 96+
 97+func toFileEntry(r *sftp.Request) *utils.FileEntry {
 98+	attrs := r.Attributes()
 99+	var size int64 = 0
100+	var mtime int64 = 0
101+	var atime int64 = 0
102+	var mode fs.FileMode
103+	if attrs != nil {
104+		mode = attrs.FileMode()
105+		size = int64(attrs.Size)
106+		mtime = int64(attrs.Mtime)
107+		atime = int64(attrs.Atime)
108+	}
109+
110+	entry := &utils.FileEntry{
111+		Filepath: r.Filepath,
112+		Mode:     mode,
113+		Size:     size,
114+		Mtime:    mtime,
115+		Atime:    atime,
116+	}
117+	return entry
118+}
119+
120+func (f *handler) Filewrite(r *sftp.Request) (io.WriterAt, error) {
121+	entry := toFileEntry(r)
122+	entry.Reader = bytes.NewReader([]byte{})
123+
124+	_, err := f.writeHandler.Write(f.session, entry)
125+	if err != nil {
126+		return nil, err
127+	}
128+
129+	buf := &buffer{}
130+	entry.Reader = buf
131+
132+	return fakeWrite{fileEntry: entry, buf: buf, handler: f}, nil
133+}
134+
135+func (f *handler) Fileread(r *sftp.Request) (io.ReaderAt, error) {
136+	if r.Filepath == "/" {
137+		return nil, os.ErrInvalid
138+	}
139+
140+	fileEntry := toFileEntry(r)
141+	_, reader, err := f.writeHandler.Read(f.session, fileEntry)
142+
143+	return reader, err
144+}
145+
146+type handlererr struct {
147+	Handler *handler
148+}
149+
150+func (f *handlererr) Filecmd(r *sftp.Request) error {
151+	err := f.Handler.Filecmd(r)
152+	if err != nil {
153+		fmt.Fprintln(f.Handler.session.Stderr(), err)
154+	}
155+	return err
156+}
157+func (f *handlererr) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
158+	result, err := f.Handler.Filelist(r)
159+	if err != nil {
160+		fmt.Fprintln(f.Handler.session.Stderr(), err)
161+	}
162+	return result, err
163+}
164+func (f *handlererr) Filewrite(r *sftp.Request) (io.WriterAt, error) {
165+	result, err := f.Handler.Filewrite(r)
166+	if err != nil {
167+		fmt.Fprintln(f.Handler.session.Stderr(), err)
168+	}
169+	return result, err
170+}
171+func (f *handlererr) Fileread(r *sftp.Request) (io.ReaderAt, error) {
172+	result, err := f.Handler.Fileread(r)
173+	if err != nil {
174+		fmt.Fprintln(f.Handler.session.Stderr(), err)
175+	}
176+	return result, err
177+}
A pkg/send/protocols/sftp/sftp.go
+69, -0
 1@@ -0,0 +1,69 @@
 2+package sftp
 3+
 4+import (
 5+	"errors"
 6+	"fmt"
 7+	"io"
 8+
 9+	"github.com/picosh/pico/pkg/pssh"
10+	"github.com/picosh/pico/pkg/send/utils"
11+	"github.com/pkg/sftp"
12+)
13+
14+// func SSHOption(writeHandler utils.CopyFromClientHandler) ssh.Option {
15+// 	return func(server *ssh.Server) error {
16+// 		if server.SubsystemHandlers == nil {
17+// 			server.SubsystemHandlers = map[string]ssh.SubsystemHandler{}
18+// 		}
19+
20+// 		server.SubsystemHandlers["sftp"] = SubsystemHandler(writeHandler)
21+// 		return nil
22+// 	}
23+// }
24+
25+func Middleware(writeHandler utils.CopyFromClientHandler) pssh.SSHServerMiddleware {
26+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
27+		return func(session *pssh.SSHServerConnSession) error {
28+			logger := writeHandler.GetLogger(session).With(
29+				"sftp", true,
30+			)
31+
32+			defer func() {
33+				if r := recover(); r != nil {
34+					logger.Error("error running sftp middleware", "err", r)
35+					fmt.Fprintln(session, "error running sftp middleware, check the flags you are using")
36+				}
37+			}()
38+
39+			err := writeHandler.Validate(session)
40+			if err != nil {
41+				fmt.Fprintln(session.Stderr(), err)
42+				return err
43+			}
44+
45+			handler := &handlererr{
46+				Handler: &handler{
47+					session:      session,
48+					writeHandler: writeHandler,
49+				},
50+			}
51+
52+			handlers := sftp.Handlers{
53+				FilePut:  handler,
54+				FileList: handler,
55+				FileGet:  handler,
56+				FileCmd:  handler,
57+			}
58+
59+			requestServer := sftp.NewRequestServer(session, handlers)
60+
61+			err = requestServer.Serve()
62+			if err != nil && !errors.Is(err, io.EOF) {
63+				fmt.Fprintln(session.Stderr(), err)
64+				logger.Error("Error serving sftp subsystem", "err", err)
65+			}
66+
67+			return err
68+		}
69+	}
70+}
A pkg/send/protocols/sftp/writer.go
+75, -0
 1@@ -0,0 +1,75 @@
 2+package sftp
 3+
 4+import (
 5+	"fmt"
 6+	"io"
 7+	"sync"
 8+
 9+	"github.com/picosh/pico/pkg/send/utils"
10+)
11+
12+type buffer struct {
13+	buf []byte
14+	m   sync.Mutex
15+	off int
16+}
17+
18+func (b *buffer) WriteAt(p []byte, pos int64) (n int, err error) {
19+	pLen := len(p)
20+	expLen := pos + int64(pLen)
21+	b.m.Lock()
22+	defer b.m.Unlock()
23+	if int64(len(b.buf)) < expLen {
24+		if int64(cap(b.buf)) < expLen {
25+			newBuf := make([]byte, expLen)
26+			copy(newBuf, b.buf)
27+			b.buf = newBuf
28+		}
29+		b.buf = b.buf[:expLen]
30+	}
31+	copy(b.buf[pos:], p)
32+	return pLen, nil
33+}
34+
35+func (b *buffer) Read(p []byte) (n int, err error) {
36+	b.m.Lock()
37+	defer b.m.Unlock()
38+	if len(b.buf) <= b.off {
39+		if len(p) == 0 {
40+			return 0, nil
41+		}
42+		return 0, io.EOF
43+	}
44+	n = copy(p, b.buf[b.off:])
45+	b.off += n
46+	return n, nil
47+}
48+
49+func (b *buffer) Close() error {
50+	b.buf = []byte{}
51+	return nil
52+}
53+
54+type fakeWrite struct {
55+	fileEntry *utils.FileEntry
56+	handler   *handler
57+	buf       *buffer
58+}
59+
60+func (f fakeWrite) WriteAt(p []byte, off int64) (int, error) {
61+	return f.buf.WriteAt(p, off)
62+}
63+
64+func (f fakeWrite) Close() error {
65+	msg, err := f.handler.writeHandler.Write(f.handler.session, f.fileEntry)
66+	if err != nil {
67+		errMsg := fmt.Sprintf("%s\r\n", err.Error())
68+		_, err = f.handler.session.Stderr().Write([]byte(errMsg))
69+	}
70+	if msg != "" {
71+		nMsg := fmt.Sprintf("%s\r\n", msg)
72+		_, err = f.handler.session.Stderr().Write([]byte(nMsg))
73+	}
74+	f.buf.Close()
75+	return err
76+}
A pkg/send/utils/file.go
+31, -0
 1@@ -0,0 +1,31 @@
 2+package utils
 3+
 4+import (
 5+	"os"
 6+	"time"
 7+)
 8+
 9+type VirtualFile struct {
10+	FName    string
11+	FIsDir   bool
12+	FSize    int64
13+	FModTime time.Time
14+	FSys     any
15+}
16+
17+func (f *VirtualFile) Name() string { return f.FName }
18+func (f *VirtualFile) Size() int64  { return f.FSize }
19+func (f *VirtualFile) Mode() os.FileMode {
20+	if f.FIsDir {
21+		return os.FileMode(0755) | os.ModeDir
22+	}
23+	return os.FileMode(0644)
24+}
25+func (f *VirtualFile) ModTime() time.Time {
26+	if f.FModTime.IsZero() {
27+		return time.Now()
28+	}
29+	return f.FModTime
30+}
31+func (f *VirtualFile) IsDir() bool { return f.FIsDir }
32+func (f *VirtualFile) Sys() any    { return f.FSys }
A pkg/send/utils/io.go
+26, -0
 1@@ -0,0 +1,26 @@
 2+package utils
 3+
 4+import (
 5+	"io"
 6+)
 7+
 8+type ReadAndReaderAt interface {
 9+	io.ReaderAt
10+	io.Reader
11+}
12+
13+type ReadAndReaderAtCloser interface {
14+	io.Reader
15+	io.ReaderAt
16+	io.ReadCloser
17+}
18+
19+func NopReadAndReaderAtCloser(r ReadAndReaderAt) ReadAndReaderAtCloser {
20+	return nopReadAndReaderAt{r}
21+}
22+
23+type nopReadAndReaderAt struct {
24+	ReadAndReaderAt
25+}
26+
27+func (nopReadAndReaderAt) Close() error { return nil }
A pkg/send/utils/limit_reader.go
+35, -0
 1@@ -0,0 +1,35 @@
 2+package utils
 3+
 4+import (
 5+	"io"
 6+	"sync"
 7+)
 8+
 9+func NewLimitReader(r io.Reader, limit int) io.Reader {
10+	return &LimitReader{
11+		r:    r,
12+		left: limit,
13+	}
14+}
15+
16+type LimitReader struct {
17+	r io.Reader
18+
19+	lock sync.Mutex
20+	left int
21+}
22+
23+func (r *LimitReader) Read(b []byte) (int, error) {
24+	r.lock.Lock()
25+	defer r.lock.Unlock()
26+
27+	if r.left <= 0 {
28+		return 0, io.EOF
29+	}
30+	if len(b) > r.left {
31+		b = b[0:r.left]
32+	}
33+	n, err := r.r.Read(b)
34+	r.left -= n
35+	return n, err
36+}
A pkg/send/utils/limit_reader_test.go
+44, -0
 1@@ -0,0 +1,44 @@
 2+package utils
 3+
 4+import (
 5+	"bytes"
 6+	"io"
 7+	"testing"
 8+
 9+	"github.com/matryer/is"
10+)
11+
12+func TestLimitedReader(t *testing.T) {
13+	t.Run("partial", func(t *testing.T) {
14+		is := is.New(t)
15+		var b bytes.Buffer
16+		b.WriteString("writing some bytes")
17+		r := NewLimitReader(&b, 7)
18+
19+		bts, err := io.ReadAll(r)
20+		is.NoErr(err)
21+		is.Equal("writing", string(bts))
22+	})
23+
24+	t.Run("full", func(t *testing.T) {
25+		is := is.New(t)
26+		var b bytes.Buffer
27+		b.WriteString("some text")
28+		r := NewLimitReader(&b, b.Len())
29+
30+		bts, err := io.ReadAll(r)
31+		is.NoErr(err)
32+		is.Equal("some text", string(bts))
33+	})
34+
35+	t.Run("pass limit", func(t *testing.T) {
36+		is := is.New(t)
37+		var b bytes.Buffer
38+		b.WriteString("another text")
39+		r := NewLimitReader(&b, b.Len()+10)
40+
41+		bts, err := io.ReadAll(r)
42+		is.NoErr(err)
43+		is.Equal("another text", string(bts))
44+	})
45+}
A pkg/send/utils/utils.go
+101, -0
  1@@ -0,0 +1,101 @@
  2+package utils
  3+
  4+import (
  5+	"encoding/base64"
  6+	"fmt"
  7+	"io"
  8+	"io/fs"
  9+	"log/slog"
 10+	"os"
 11+	"path/filepath"
 12+	"strconv"
 13+
 14+	"github.com/picosh/pico/pkg/pssh"
 15+)
 16+
 17+// NULL is an array with a single NULL byte.
 18+var NULL = []byte{'\x00'}
 19+
 20+// FileEntry is an Entry that reads from a Reader, defining a file and
 21+// its contents.
 22+type FileEntry struct {
 23+	Filepath string
 24+	Mode     fs.FileMode
 25+	Size     int64
 26+	Reader   io.Reader
 27+	Atime    int64
 28+	Mtime    int64
 29+	Metadata map[string]string
 30+}
 31+
 32+// Write a file to the given writer.
 33+func (e *FileEntry) Write(w io.Writer) error {
 34+	if e.Mtime > 0 && e.Atime > 0 {
 35+		if _, err := fmt.Fprintf(w, "T%d 0 %d 0\n", e.Mtime, e.Atime); err != nil {
 36+			return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
 37+		}
 38+	}
 39+	fname := filepath.Base(e.Filepath)
 40+	if _, err := fmt.Fprintf(w, "C%s %d %s\n", octalPerms(e.Mode), e.Size, fname); err != nil {
 41+		return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
 42+	}
 43+
 44+	if _, err := io.Copy(w, e.Reader); err != nil {
 45+		return fmt.Errorf("failed to read file: %q: %w", e.Filepath, err)
 46+	}
 47+
 48+	if _, err := w.Write(NULL); err != nil {
 49+		return fmt.Errorf("failed to write file: %q: %w", e.Filepath, err)
 50+	}
 51+	return nil
 52+}
 53+
 54+func octalPerms(info fs.FileMode) string {
 55+	return "0" + strconv.FormatUint(uint64(info.Perm()), 8)
 56+}
 57+
 58+// CopyFromClientHandler is a handler that can be implemented to handle files
 59+// being copied from the client to the server.
 60+type CopyFromClientHandler interface {
 61+	// Write should write the given file.
 62+	Delete(*pssh.SSHServerConnSession, *FileEntry) error
 63+	Write(*pssh.SSHServerConnSession, *FileEntry) (string, error)
 64+	Read(*pssh.SSHServerConnSession, *FileEntry) (os.FileInfo, ReadAndReaderAtCloser, error)
 65+	List(*pssh.SSHServerConnSession, string, bool, bool) ([]os.FileInfo, error)
 66+	GetLogger(*pssh.SSHServerConnSession) *slog.Logger
 67+	Validate(*pssh.SSHServerConnSession) error
 68+}
 69+
 70+func KeyText(session *pssh.SSHServerConnSession) (string, error) {
 71+	if session.PublicKey() == nil {
 72+		return "", fmt.Errorf("session doesn't have public key")
 73+	}
 74+	kb := base64.StdEncoding.EncodeToString(session.PublicKey().Marshal())
 75+	return fmt.Sprintf("%s %s", session.PublicKey().Type(), kb), nil
 76+}
 77+
 78+func ErrorHandler(session *pssh.SSHServerConnSession, err error) {
 79+	_, _ = fmt.Fprint(session.Stderr(), err, "\r\n")
 80+	_ = session.Exit(1)
 81+	_ = session.Close()
 82+}
 83+
 84+func PrintMsg(session *pssh.SSHServerConnSession, stdout []string, stderr []error) {
 85+	output := ""
 86+	if len(stdout) > 0 {
 87+		for _, msg := range stdout {
 88+			if msg != "" {
 89+				output += fmt.Sprintf("%s\r\n", msg)
 90+			}
 91+		}
 92+		_, _ = fmt.Fprintln(session.Stderr(), output)
 93+	}
 94+
 95+	outputErr := ""
 96+	if len(stderr) > 0 {
 97+		for _, err := range stderr {
 98+			outputErr += fmt.Sprintf("%v\r\n", err)
 99+		}
100+		_, _ = fmt.Fprintln(session.Stderr(), outputErr)
101+	}
102+}
R shared/analytics.go => pkg/shared/analytics.go
+1, -1
1@@ -15,7 +15,7 @@ import (
2 	"strings"
3 	"time"
4 
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils/pipe/metrics"
8 	"github.com/simplesurance/go-ip-anonymizer/ipanonymizer"
9 	"github.com/x-way/crawlerdetect"
R shared/api.go => pkg/shared/api.go
+2, -2
 1@@ -8,9 +8,9 @@ import (
 2 	"os"
 3 	"strings"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7+	"github.com/picosh/pico/pkg/db"
 8 	"github.com/picosh/utils"
 9+	"golang.org/x/crypto/ssh"
10 )
11 
12 type SubdomainProps struct {
R shared/bucket.go => pkg/shared/bucket.go
+1, -1
1@@ -6,7 +6,7 @@ import (
2 	"path/filepath"
3 	"strings"
4 
5-	"github.com/picosh/send/utils"
6+	"github.com/picosh/pico/pkg/send/utils"
7 )
8 
9 func GetImgsBucketName(userID string) string {
R shared/config.go => pkg/shared/config.go
+1, -1
1@@ -12,7 +12,7 @@ import (
2 	"strings"
3 	"time"
4 
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 
9 	pipeLogger "github.com/picosh/utils/pipe/log"
R shared/feed.go => pkg/shared/feed.go
+1, -1
1@@ -5,7 +5,7 @@ import (
2 	"time"
3 
4 	"github.com/gorilla/feeds"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 )
8 
9 func UserFeed(me db.DB, userID, token string) (*feeds.Feed, error) {
R shared/listparser.go => pkg/shared/listparser.go
+0, -0
R shared/mdparser.go => pkg/shared/mdparser.go
+0, -0
R shared/pubsub.go => pkg/shared/pubsub.go
+0, -0
R shared/router.go => pkg/shared/router.go
+7, -7
 1@@ -10,9 +10,9 @@ import (
 2 	"regexp"
 3 	"strings"
 4 
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/shared/storage"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/pssh"
10+	"github.com/picosh/pico/pkg/shared/storage"
11 )
12 
13 type Route struct {
14@@ -173,12 +173,12 @@ type ctxCfg struct{}
15 
16 type CtxSubdomainKey struct{}
17 type ctxKey struct{}
18-type CtxSshKey struct{}
19+type CtxSessionKey struct{}
20 
21-func GetSshCtx(r *http.Request) (ssh.Context, error) {
22-	payload, ok := r.Context().Value(CtxSshKey{}).(ssh.Context)
23+func GetSshCtx(r *http.Request) (*pssh.SSHServerConnSession, error) {
24+	payload, ok := r.Context().Value(CtxSessionKey{}).(*pssh.SSHServerConnSession)
25 	if payload == nil || !ok {
26-		return payload, fmt.Errorf("sshCtx not set on `r.Context()` for connection")
27+		return payload, fmt.Errorf("ssh session not set on `r.Context()` for connection")
28 	}
29 	return payload, nil
30 }
R shared/senpai.go => pkg/shared/senpai.go
+12, -8
 1@@ -6,8 +6,8 @@ import (
 2 	"sync"
 3 
 4 	"git.sr.ht/~delthas/senpai"
 5-	"github.com/charmbracelet/ssh"
 6 	"github.com/containerd/console"
 7+	"github.com/picosh/pico/pkg/pssh"
 8 )
 9 
10 type consoleData struct {
11@@ -16,13 +16,13 @@ type consoleData struct {
12 }
13 
14 type VConsole struct {
15-	ssh.Session
16-	pty ssh.Pty
17+	Session *pssh.SSHServerConnSession
18+	pty     *pssh.Pty
19 
20 	sizeEnableOnce sync.Once
21 
22 	windowMu      sync.Mutex
23-	currentWindow ssh.Window
24+	currentWindow pssh.Window
25 
26 	readReq  chan []byte
27 	dataChan chan consoleData
28@@ -94,7 +94,7 @@ func (v *VConsole) DisableEcho() error {
29 }
30 
31 func (v *VConsole) Reset() error {
32-	_, err := v.Write([]byte("\033[?25h\033[0 q\033[34h\033[?25h\033[39;49m\033[m^O\033[H\033[J\033[?1049l\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l"))
33+	_, err := v.Session.Write([]byte("\033[?25h\033[0 q\033[34h\033[?25h\033[39;49m\033[m^O\033[H\033[J\033[?1049l\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l"))
34 	return err
35 }
36 
37@@ -108,7 +108,7 @@ func (v *VConsole) Size() (console.WinSize, error) {
38 }
39 
40 func (v *VConsole) Fd() uintptr {
41-	return v.pty.Slave.Fd()
42+	return 0
43 }
44 
45 func (v *VConsole) Name() string {
46@@ -123,7 +123,11 @@ func (v *VConsole) Close() error {
47 	return err
48 }
49 
50-func NewVConsole(sesh ssh.Session) (*VConsole, error) {
51+func (v *VConsole) Write(p []byte) (int, error) {
52+	return v.Session.Write(p)
53+}
54+
55+func NewVConsole(sesh *pssh.SSHServerConnSession) (*VConsole, error) {
56 	pty, win, ok := sesh.Pty()
57 	if !ok {
58 		return nil, fmt.Errorf("PTY not found")
59@@ -178,7 +182,7 @@ func NewVConsole(sesh ssh.Session) (*VConsole, error) {
60 	return vty, nil
61 }
62 
63-func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
64+func NewSenpaiApp(sesh *pssh.SSHServerConnSession, username, pass string) (*senpai.App, error) {
65 	vty, err := NewVConsole(sesh)
66 	if err != nil {
67 		slog.Error("PTY not found")
R shared/ssh.go => pkg/shared/ssh.go
+12, -11
 1@@ -1,11 +1,12 @@
 2 package shared
 3 
 4 import (
 5+	"fmt"
 6 	"log/slog"
 7 
 8-	"github.com/charmbracelet/ssh"
 9-	"github.com/picosh/pico/db"
10+	"github.com/picosh/pico/pkg/db"
11 	"github.com/picosh/utils"
12+	"golang.org/x/crypto/ssh"
13 )
14 
15 type SshAuthHandler struct {
16@@ -24,7 +25,7 @@ func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger) *SshAuthHandler {
17 	}
18 }
19 
20-func (r *SshAuthHandler) PubkeyAuthHandler(ctx ssh.Context, key ssh.PublicKey) bool {
21+func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
22 	pubkey := utils.KeyForKeyText(key)
23 	user, err := r.DB.FindUserByPubkey(pubkey)
24 	if err != nil {
25@@ -34,20 +35,20 @@ func (r *SshAuthHandler) PubkeyAuthHandler(ctx ssh.Context, key ssh.PublicKey) b
26 			"key", string(key.Marshal()),
27 			"err", err,
28 		)
29-		return false
30+		return nil, err
31 	}
32 
33 	if user.Name == "" {
34 		r.Logger.Error("username is not set")
35-		return false
36+		return nil, fmt.Errorf("username is not set")
37 	}
38 
39-	if ctx.Permissions().Extensions == nil {
40-		ctx.Permissions().Extensions = map[string]string{}
41-	}
42-	ctx.Permissions().Extensions["user_id"] = user.ID
43-	ctx.Permissions().Extensions["pubkey"] = pubkey
44-	return true
45+	return &ssh.Permissions{
46+		Extensions: map[string]string{
47+			"user_id": user.ID,
48+			"pubkey":  pubkey,
49+		},
50+	}, nil
51 }
52 
53 func FindPlusFF(dbpool db.DB, cfg *ConfigSite, userID string) *db.FeatureFlag {
R shared/storage/fs.go => pkg/shared/storage/fs.go
+1, -1
1@@ -8,7 +8,7 @@ import (
2 	"path/filepath"
3 	"strings"
4 
5-	sst "github.com/picosh/pobj/storage"
6+	sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8 
9 type StorageFS struct {
R shared/storage/memory.go => pkg/shared/storage/memory.go
+1, -1
1@@ -5,7 +5,7 @@ import (
2 	"net/http"
3 	"time"
4 
5-	sst "github.com/picosh/pobj/storage"
6+	sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8 
9 type StorageMemory struct {
R shared/storage/minio.go => pkg/shared/storage/minio.go
+1, -1
1@@ -8,7 +8,7 @@ import (
2 	"path/filepath"
3 	"strings"
4 
5-	sst "github.com/picosh/pobj/storage"
6+	sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8 
9 type StorageMinio struct {
R shared/storage/proxy.go => pkg/shared/storage/proxy.go
+1, -1
1@@ -15,7 +15,7 @@ import (
2 	"strings"
3 	"time"
4 
5-	"github.com/picosh/pobj/storage"
6+	"github.com/picosh/pico/pkg/pobj/storage"
7 )
8 
9 func GetMimeType(fpath string) string {
R shared/storage/proxy_test.go => pkg/shared/storage/proxy_test.go
+0, -0
R shared/storage/ratio.go => pkg/shared/storage/ratio.go
+0, -0
R shared/storage/storage.go => pkg/shared/storage/storage.go
+1, -1
1@@ -3,7 +3,7 @@ package storage
2 import (
3 	"io"
4 
5-	sst "github.com/picosh/pobj/storage"
6+	sst "github.com/picosh/pico/pkg/pobj/storage"
7 )
8 
9 type StorageServe interface {
R shared/tunnel.go => pkg/shared/tunnel.go
+0, -0
R tui/analytics.go => pkg/tui/analytics.go
+1, -1
1@@ -10,7 +10,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis/vxfw/list"
3 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 )
9 
R tui/border.go => pkg/tui/border.go
+0, -0
R tui/chat.go => pkg/tui/chat.go
+0, -0
R tui/group.go => pkg/tui/group.go
+0, -0
R tui/info.go => pkg/tui/info.go
+1, -1
1@@ -7,7 +7,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis"
3 	"git.sr.ht/~rockorager/vaxis/vxfw"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 )
8 
9 type UsageInfo struct {
R tui/input.go => pkg/tui/input.go
+0, -0
R tui/kv.go => pkg/tui/kv.go
+0, -0
R tui/logs.go => pkg/tui/logs.go
+1, -1
1@@ -13,7 +13,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis/vxfw/list"
3 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 	"github.com/picosh/utils"
8 	pipeLogger "github.com/picosh/utils/pipe/log"
9 )
R tui/menu.go => pkg/tui/menu.go
+1, -1
1@@ -5,7 +5,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis/vxfw"
3 	"git.sr.ht/~rockorager/vaxis/vxfw/list"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 )
8 
9 var menuChoices = []string{
R tui/pager.go => pkg/tui/pager.go
+0, -0
R tui/plus.go => pkg/tui/plus.go
+0, -0
R tui/pubkeys.go => pkg/tui/pubkeys.go
+1, -1
1@@ -10,7 +10,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis/vxfw/list"
3 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 	"golang.org/x/crypto/ssh"
9 )
R tui/senpai.go => pkg/tui/senpai.go
+1, -1
1@@ -3,7 +3,7 @@ package tui
2 import (
3 	"io"
4 
5-	"github.com/picosh/pico/shared"
6+	"github.com/picosh/pico/pkg/shared"
7 )
8 
9 type SenpaiCmd struct {
R tui/signup.go => pkg/tui/signup.go
+1, -1
1@@ -8,7 +8,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis/vxfw/button"
3 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 	"github.com/picosh/utils"
8 	"golang.org/x/crypto/ssh"
9 )
R tui/tokens.go => pkg/tui/tokens.go
+1, -1
1@@ -10,7 +10,7 @@ import (
2 	"git.sr.ht/~rockorager/vaxis/vxfw/list"
3 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
4 	"git.sr.ht/~rockorager/vaxis/vxfw/text"
5-	"github.com/picosh/pico/db"
6+	"github.com/picosh/pico/pkg/db"
7 )
8 
9 type TokensPage struct {
R tui/ui.go => pkg/tui/ui.go
+4, -4
 1@@ -9,9 +9,9 @@ import (
 2 	"git.sr.ht/~rockorager/vaxis"
 3 	"git.sr.ht/~rockorager/vaxis/vxfw"
 4 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
 5-	"github.com/charmbracelet/ssh"
 6-	"github.com/picosh/pico/db"
 7-	"github.com/picosh/pico/shared"
 8+	"github.com/picosh/pico/pkg/db"
 9+	"github.com/picosh/pico/pkg/pssh"
10+	"github.com/picosh/pico/pkg/shared"
11 	"github.com/picosh/utils"
12 )
13 
14@@ -19,7 +19,7 @@ var HOME = "dash"
15 
16 type SharedModel struct {
17 	Logger             *slog.Logger
18-	Session            ssh.Session
19+	Session            *pssh.SSHServerConnSession
20 	Cfg                *shared.ConfigSite
21 	Dbpool             db.DB
22 	User               *db.User
A pkg/tunkit/ptun.go
+108, -0
  1@@ -0,0 +1,108 @@
  2+package tunkit
  3+
  4+import (
  5+	"errors"
  6+	"io"
  7+	"log/slog"
  8+	"net"
  9+	"sync"
 10+
 11+	"github.com/picosh/pico/pkg/pssh"
 12+	"golang.org/x/crypto/ssh"
 13+)
 14+
 15+type forwardedTCPPayload struct {
 16+	Addr       string
 17+	Port       uint32
 18+	OriginAddr string
 19+	OriginPort uint32
 20+}
 21+
 22+type Tunnel interface {
 23+	CreateConn(ctx *pssh.SSHServerConnSession) (net.Conn, error)
 24+	GetLogger() *slog.Logger
 25+	Close(ctx *pssh.SSHServerConnSession) error
 26+}
 27+
 28+func LocalForwardHandler(handler Tunnel) pssh.SSHServerChannelMiddleware {
 29+	return func(newChan ssh.NewChannel, sc *pssh.SSHServerConn) error {
 30+		check := &forwardedTCPPayload{}
 31+		err := ssh.Unmarshal(newChan.ExtraData(), check)
 32+		logger := handler.GetLogger()
 33+		if err != nil {
 34+			logger.Error(
 35+				"error unmarshaling information",
 36+				"err", err,
 37+			)
 38+			return err
 39+		}
 40+
 41+		log := logger.With(
 42+			"addr", check.Addr,
 43+			"port", check.Port,
 44+			"origAddr", check.OriginAddr,
 45+			"origPort", check.OriginPort,
 46+		)
 47+		log.Info("local forward request")
 48+
 49+		ch, reqs, err := newChan.Accept()
 50+		if err != nil {
 51+			log.Error("cannot accept new channel", "err", err)
 52+			return err
 53+		}
 54+
 55+		ctx := &pssh.SSHServerConnSession{
 56+			Channel:       ch,
 57+			SSHServerConn: sc,
 58+		}
 59+
 60+		go ssh.DiscardRequests(reqs)
 61+
 62+		go func() {
 63+			downConn, err := handler.CreateConn(ctx)
 64+			if err != nil {
 65+				log.Error("unable to connect to conn", "err", err)
 66+				ch.Close()
 67+				return
 68+			}
 69+			defer downConn.Close()
 70+
 71+			var wg sync.WaitGroup
 72+			wg.Add(2)
 73+
 74+			go func() {
 75+				defer wg.Done()
 76+				defer func() {
 77+					_ = ch.CloseWrite()
 78+				}()
 79+				defer downConn.Close()
 80+				_, err := io.Copy(ch, downConn)
 81+				if err != nil {
 82+					if !errors.Is(err, net.ErrClosed) {
 83+						log.Error("io copy", "err", err)
 84+					}
 85+				}
 86+			}()
 87+			go func() {
 88+				defer wg.Done()
 89+				defer ch.Close()
 90+				defer downConn.Close()
 91+				_, err := io.Copy(downConn, ch)
 92+				if err != nil {
 93+					if !errors.Is(err, net.ErrClosed) {
 94+						log.Error("io copy", "err", err)
 95+					}
 96+				}
 97+			}()
 98+
 99+			wg.Wait()
100+		}()
101+
102+		<-ctx.Done()
103+		err = handler.Close(ctx)
104+		if err != nil {
105+			log.Error("tunnel handler error", "err", err)
106+		}
107+		return err
108+	}
109+}
A pkg/tunkit/web-handler.go
+92, -0
 1@@ -0,0 +1,92 @@
 2+package tunkit
 3+
 4+import (
 5+	"fmt"
 6+	"log/slog"
 7+	"net"
 8+	"os"
 9+
10+	"github.com/picosh/pico/pkg/pssh"
11+)
12+
13+type ctxAddressKey struct{}
14+
15+func getAddressCtx(ctx *pssh.SSHServerConnSession) (string, error) {
16+	address, ok := ctx.Value(ctxAddressKey{}).(string)
17+	if address == "" || !ok {
18+		return address, fmt.Errorf("address not set on `*pssh.SSHServerConnSession()` for connection")
19+	}
20+	return address, nil
21+}
22+func setAddressCtx(ctx *pssh.SSHServerConnSession, address string) {
23+	ctx.SetValue(ctxAddressKey{}, address)
24+}
25+
26+type WebTunnelHandler struct {
27+	HttpHandler HttpHandlerFn
28+	Logger      *slog.Logger
29+}
30+
31+func NewWebTunnelHandler(handler HttpHandlerFn, logger *slog.Logger) *WebTunnelHandler {
32+	return &WebTunnelHandler{
33+		HttpHandler: handler,
34+		Logger:      logger,
35+	}
36+}
37+
38+func (wt *WebTunnelHandler) GetLogger() *slog.Logger {
39+	return wt.Logger
40+}
41+
42+func (wt *WebTunnelHandler) GetHttpHandler() HttpHandlerFn {
43+	return wt.HttpHandler
44+}
45+
46+func (wt *WebTunnelHandler) Close(ctx *pssh.SSHServerConnSession) error {
47+	listener, err := getListenerCtx(ctx)
48+	if err != nil {
49+		return err
50+	}
51+
52+	if listener != nil {
53+		_ = listener.Close()
54+		setListenerCtx(ctx, nil)
55+	}
56+
57+	return nil
58+}
59+
60+func (wt *WebTunnelHandler) CreateListener(ctx *pssh.SSHServerConnSession) (net.Listener, error) {
61+	tempFile, err := os.CreateTemp("", "")
62+	if err != nil {
63+		return nil, err
64+	}
65+
66+	tempFile.Close()
67+	address := tempFile.Name()
68+	os.Remove(address)
69+
70+	connListener, err := net.Listen("unix", address)
71+	if err != nil {
72+		return nil, err
73+	}
74+	setAddressCtx(ctx, address)
75+	setListenerCtx(ctx, connListener)
76+
77+	return connListener, nil
78+}
79+
80+func (wt *WebTunnelHandler) CreateConn(ctx *pssh.SSHServerConnSession) (net.Conn, error) {
81+	_, err := httpServe(wt, ctx, wt.GetLogger())
82+	if err != nil {
83+		wt.GetLogger().Info("unable to create listener", "err", err)
84+		return nil, err
85+	}
86+
87+	address, err := getAddressCtx(ctx)
88+	if err != nil {
89+		return nil, err
90+	}
91+
92+	return net.Dial("unix", address)
93+}
A pkg/tunkit/web.go
+57, -0
 1@@ -0,0 +1,57 @@
 2+package tunkit
 3+
 4+import (
 5+	"fmt"
 6+	"log/slog"
 7+	"net"
 8+	"net/http"
 9+
10+	"github.com/picosh/pico/pkg/pssh"
11+)
12+
13+type HttpHandlerFn = func(ctx *pssh.SSHServerConnSession) http.Handler
14+
15+type WebTunnel interface {
16+	GetHttpHandler() HttpHandlerFn
17+	CreateListener(ctx *pssh.SSHServerConnSession) (net.Listener, error)
18+	CreateConn(ctx *pssh.SSHServerConnSession) (net.Conn, error)
19+	GetLogger() *slog.Logger
20+	Close(ctx *pssh.SSHServerConnSession) error
21+}
22+
23+type ctxListenerKey struct{}
24+
25+func getListenerCtx(ctx *pssh.SSHServerConnSession) (net.Listener, error) {
26+	listener, ok := ctx.Value(ctxListenerKey{}).(net.Listener)
27+	if listener == nil || !ok {
28+		return nil, fmt.Errorf("listener not set on `*pssh.SSHServerConnSession()` for connection")
29+	}
30+	return listener, nil
31+}
32+
33+func setListenerCtx(ctx *pssh.SSHServerConnSession, listener net.Listener) {
34+	ctx.SetValue(ctxListenerKey{}, listener)
35+}
36+
37+func httpServe(handler WebTunnel, ctx *pssh.SSHServerConnSession, log *slog.Logger) (net.Listener, error) {
38+	cached, _ := getListenerCtx(ctx)
39+	if cached != nil {
40+		return cached, nil
41+	}
42+
43+	listener, err := handler.CreateListener(ctx)
44+	if err != nil {
45+		return nil, err
46+	}
47+	setListenerCtx(ctx, listener)
48+
49+	go func() {
50+		httpHandler := handler.GetHttpHandler()
51+		err := http.Serve(listener, httpHandler(ctx))
52+		if err != nil {
53+			log.Error("serving http content", "err", err)
54+		}
55+	}()
56+
57+	return listener, nil
58+}
D prose/ssh.go
+0, -130
  1@@ -1,130 +0,0 @@
  2-package prose
  3-
  4-import (
  5-	"context"
  6-	"fmt"
  7-	"os"
  8-	"os/signal"
  9-	"syscall"
 10-	"time"
 11-
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15-	"github.com/picosh/pico/db/postgres"
 16-	"github.com/picosh/pico/filehandlers"
 17-	uploadimgs "github.com/picosh/pico/filehandlers/imgs"
 18-	"github.com/picosh/pico/shared"
 19-	"github.com/picosh/pico/shared/storage"
 20-	wsh "github.com/picosh/pico/wish"
 21-	"github.com/picosh/send/auth"
 22-	"github.com/picosh/send/list"
 23-	"github.com/picosh/send/pipe"
 24-	wishrsync "github.com/picosh/send/protocols/rsync"
 25-	"github.com/picosh/send/protocols/scp"
 26-	"github.com/picosh/send/protocols/sftp"
 27-	"github.com/picosh/send/proxy"
 28-	"github.com/picosh/utils"
 29-)
 30-
 31-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 32-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 33-		return []wish.Middleware{
 34-			pipe.Middleware(handler, ".md"),
 35-			list.Middleware(handler),
 36-			scp.Middleware(handler),
 37-			wishrsync.Middleware(handler),
 38-			auth.Middleware(handler),
 39-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 40-			wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool),
 41-		}
 42-	}
 43-}
 44-
 45-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 46-	return func(server *ssh.Server) error {
 47-		err := sftp.SSHOption(handler)(server)
 48-		if err != nil {
 49-			return err
 50-		}
 51-
 52-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 53-
 54-		for name, subsystemHandler := range server.SubsystemHandlers {
 55-			newSubsystemHandlers[name] = func(s ssh.Session) {
 56-				wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool)(ssh.Handler(subsystemHandler))(s)
 57-			}
 58-		}
 59-
 60-		server.SubsystemHandlers = newSubsystemHandlers
 61-
 62-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 63-	}
 64-}
 65-
 66-func StartSshServer() {
 67-	host := utils.GetEnv("PROSE_HOST", "0.0.0.0")
 68-	port := utils.GetEnv("PROSE_SSH_PORT", "2222")
 69-	promPort := utils.GetEnv("PROSE_PROM_PORT", "9222")
 70-	cfg := NewConfigSite("prose-ssh")
 71-	logger := cfg.Logger
 72-	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 73-	defer dbh.Close()
 74-
 75-	hooks := &MarkdownHooks{
 76-		Cfg: cfg,
 77-		Db:  dbh,
 78-	}
 79-
 80-	var st storage.StorageServe
 81-	var err error
 82-	if cfg.MinioURL == "" {
 83-		st, err = storage.NewStorageFS(cfg.Logger, cfg.StorageDir)
 84-	} else {
 85-		st, err = storage.NewStorageMinio(cfg.Logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
 86-	}
 87-
 88-	if err != nil {
 89-		logger.Error("storage", "err", err.Error())
 90-		return
 91-	}
 92-
 93-	fileMap := map[string]filehandlers.ReadWriteHandler{
 94-		".md":      filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 95-		".css":     filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 96-		"fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
 97-	}
 98-	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 99-
100-	sshAuth := shared.NewSshAuthHandler(dbh, logger)
101-	s, err := wish.NewServer(
102-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
103-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
104-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
105-		withProxy(
106-			handler,
107-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "prose-ssh"),
108-		),
109-	)
110-	if err != nil {
111-		logger.Error("wish server", "err", err.Error())
112-		return
113-	}
114-
115-	done := make(chan os.Signal, 1)
116-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
117-	logger.Info("Starting SSH server", "host", host, "port", port)
118-	go func() {
119-		if err = s.ListenAndServe(); err != nil {
120-			logger.Error(err.Error())
121-		}
122-	}()
123-
124-	<-done
125-	logger.Info("Stopping SSH server")
126-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
127-	defer func() { cancel() }()
128-	if err := s.Shutdown(ctx); err != nil {
129-		logger.Error(err.Error())
130-	}
131-}
D wish/logger.go
+0, -88
 1@@ -1,88 +0,0 @@
 2-package wish
 3-
 4-import (
 5-	"log/slog"
 6-	"time"
 7-
 8-	"github.com/charmbracelet/ssh"
 9-	"github.com/charmbracelet/wish"
10-	"github.com/picosh/pico/db"
11-	"github.com/picosh/pico/shared"
12-)
13-
14-type ctxLoggerKey struct{}
15-type ctxUserKey struct{}
16-
17-type FindUserInterface interface {
18-	FindUserByPubkey(string) (*db.User, error)
19-}
20-
21-func LogMiddleware(defaultLogger *slog.Logger, db FindUserInterface) wish.Middleware {
22-	return func(sh ssh.Handler) ssh.Handler {
23-		return func(s ssh.Session) {
24-			ct := time.Now()
25-
26-			logger := GetLogger(s)
27-			if logger == slog.Default() {
28-				logger = defaultLogger
29-
30-				user := GetUser(s)
31-				if user == nil {
32-					user, err := db.FindUserByPubkey(s.Permissions().Extensions["pubkey"])
33-					if err == nil && user != nil {
34-						logger = shared.LoggerWithUser(logger, user).With(
35-							"ip", s.RemoteAddr().String(),
36-						)
37-						s.Context().SetValue(ctxUserKey{}, user)
38-					}
39-				}
40-
41-				s.Context().SetValue(ctxLoggerKey{}, logger)
42-			}
43-
44-			pty, _, ok := s.Pty()
45-
46-			logger.Info(
47-				"connect",
48-				"sshUser", s.User(),
49-				"pty", ok,
50-				"term", pty.Term,
51-				"windowWidth", pty.Window.Width,
52-				"windowHeight", pty.Window.Height,
53-			)
54-
55-			sh(s)
56-
57-			logger.Info(
58-				"disconnect",
59-				"sshUser", s.User(),
60-				"pty", ok,
61-				"term", pty.Term,
62-				"windowWidth", pty.Window.Width,
63-				"windowHeight", pty.Window.Height,
64-				"duration", time.Since(ct),
65-			)
66-		}
67-	}
68-}
69-
70-func GetLogger(s ssh.Session) *slog.Logger {
71-	logger := slog.Default()
72-	if s == nil {
73-		return logger
74-	}
75-
76-	if v, ok := s.Context().Value(ctxLoggerKey{}).(*slog.Logger); ok {
77-		return v
78-	}
79-
80-	return logger
81-}
82-
83-func GetUser(s ssh.Session) *db.User {
84-	if v, ok := s.Context().Value(ctxUserKey{}).(*db.User); ok {
85-		return v
86-	}
87-
88-	return nil
89-}
D wish/pty.go
+0, -39
 1@@ -1,39 +0,0 @@
 2-package wish
 3-
 4-import (
 5-	"fmt"
 6-
 7-	"github.com/charmbracelet/ssh"
 8-	"github.com/charmbracelet/wish"
 9-)
10-
11-func SessionMessage(sesh ssh.Session, msg string) {
12-	_, _ = sesh.Write([]byte(msg + "\r\n"))
13-}
14-
15-func DeprecatedNotice() wish.Middleware {
16-	return func(next ssh.Handler) ssh.Handler {
17-		return func(sesh ssh.Session) {
18-			msg := fmt.Sprintf(
19-				"%s\n\nRun %s to access pico's TUI",
20-				"DEPRECATED",
21-				"ssh pico.sh",
22-			)
23-			SessionMessage(sesh, msg)
24-			next(sesh)
25-		}
26-	}
27-}
28-
29-func PtyMdw(mdw wish.Middleware) wish.Middleware {
30-	return func(next ssh.Handler) ssh.Handler {
31-		return func(sesh ssh.Session) {
32-			_, _, ok := sesh.Pty()
33-			if !ok {
34-				next(sesh)
35-				return
36-			}
37-			mdw(next)(sesh)
38-		}
39-	}
40-}