everything you need to create an atproto appview

init: extract important bits from tangled

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 5d5ec78c

+1803
+26
.env.example
··· 1 + # development mode (set to false for production) 2 + APPVIEW_DEV=true 3 + 4 + # server configuration 5 + APPVIEW_HOST=localhost:3000 6 + APPVIEW_LISTEN_ADDR=0.0.0.0:3000 7 + APPVIEW_NAME=ATProto Starter Kit 8 + 9 + # database 10 + APPVIEW_DB_PATH=appview.db 11 + 12 + # cookie secret (generate with: openssl rand -hex 32) 13 + APPVIEW_COOKIE_SECRET=00000000000000000000000000000000 14 + 15 + # oauth configuration 16 + # generate keypair with: goat keygen 17 + APPVIEW_OAUTH_CLIENT_SECRET=z42tjbmA9q6g99qKdyBjGHrQC7EPHwH9pDwjwG5E71PMSNJH 18 + APPVIEW_OAUTH_CLIENT_KID=1770996715 19 + 20 + # redis configuration 21 + APPVIEW_REDIS_ADDR=localhost:6379 22 + APPVIEW_REDIS_PASS= 23 + APPVIEW_REDIS_DB=1 24 + 25 + # plc directory 26 + APPVIEW_PLC_URL=https://plc.directory
+5
.gitignore
··· 1 + bin 2 + *.db 3 + *.db-shm 4 + *.db-wal 5 + .env
+76
appview/config/config.go
··· 1 + package config 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net" 7 + "net/url" 8 + 9 + "github.com/sethvargo/go-envconfig" 10 + ) 11 + 12 + type CoreConfig struct { 13 + CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"` 14 + DbPath string `env:"DB_PATH, default=appview.db"` 15 + ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"` 16 + Host string `env:"HOST, default=localhost:3000"` 17 + Name string `env:"NAME, default=ATProto Starter Kit"` 18 + Dev bool `env:"DEV, default=true"` 19 + } 20 + 21 + func (c *CoreConfig) BaseUrl() string { 22 + if c.Dev { 23 + return "http://" + c.Host 24 + } 25 + return "https://" + c.Host 26 + } 27 + 28 + func (c *CoreConfig) Port() string { 29 + _, port, err := net.SplitHostPort(c.ListenAddr) 30 + if err != nil { 31 + return "3000" 32 + } 33 + return port 34 + } 35 + 36 + type OAuthConfig struct { 37 + ClientSecret string `env:"CLIENT_SECRET"` 38 + ClientKid string `env:"CLIENT_KID"` 39 + } 40 + 41 + type RedisConfig struct { 42 + Addr string `env:"ADDR, default=localhost:6379"` 43 + Password string `env:"PASS"` 44 + DB int `env:"DB, default=0"` 45 + } 46 + 47 + func (cfg RedisConfig) ToURL() string { 48 + u := &url.URL{ 49 + Scheme: "redis", 50 + Host: cfg.Addr, 51 + Path: fmt.Sprintf("/%d", cfg.DB), 52 + } 53 + if cfg.Password != "" { 54 + u.User = url.UserPassword("", cfg.Password) 55 + } 56 + return u.String() 57 + } 58 + 59 + type PlcConfig struct { 60 + PLCURL string `env:"URL, default=https://plc.directory"` 61 + } 62 + 63 + type Config struct { 64 + Core CoreConfig `env:",prefix=APPVIEW_"` 65 + OAuth OAuthConfig `env:",prefix=APPVIEW_OAUTH_"` 66 + Redis RedisConfig `env:",prefix=APPVIEW_REDIS_"` 67 + Plc PlcConfig `env:",prefix=APPVIEW_PLC_"` 68 + } 69 + 70 + func LoadConfig(ctx context.Context) (*Config, error) { 71 + var cfg Config 72 + if err := envconfig.Process(ctx, &cfg); err != nil { 73 + return nil, err 74 + } 75 + return &cfg, nil 76 + }
+52
appview/db/db.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "strings" 7 + 8 + "github.com/charmbracelet/log" 9 + _ "github.com/mattn/go-sqlite3" 10 + ) 11 + 12 + type DB struct { 13 + *sql.DB 14 + logger *log.Logger 15 + } 16 + 17 + type Execer interface { 18 + Query(query string, args ...any) (*sql.Rows, error) 19 + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) 20 + QueryRow(query string, args ...any) *sql.Row 21 + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row 22 + Exec(query string, args ...any) (sql.Result, error) 23 + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) 24 + Prepare(query string) (*sql.Stmt, error) 25 + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 26 + } 27 + 28 + func Make(ctx context.Context, dbPath string, logger *log.Logger) (*DB, error) { 29 + opts := []string{ 30 + "_foreign_keys=1", 31 + "_journal_mode=WAL", 32 + "_synchronous=NORMAL", 33 + "_auto_vacuum=incremental", 34 + } 35 + 36 + db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 37 + if err != nil { 38 + return nil, err 39 + } 40 + 41 + conn, err := db.Conn(ctx) 42 + if err != nil { 43 + return nil, err 44 + } 45 + defer conn.Close() 46 + 47 + return &DB{db, logger}, nil 48 + } 49 + 50 + func (d *DB) Close() error { 51 + return d.DB.Close() 52 + }
+86
appview/oauth/oauth.go
··· 1 + package oauth 2 + 3 + import ( 4 + "fmt" 5 + "net" 6 + "net/http" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 + atpclient "github.com/bluesky-social/indigo/atproto/client" 11 + atcrypto "github.com/bluesky-social/indigo/atproto/crypto" 12 + "github.com/charmbracelet/log" 13 + "github.com/gorilla/sessions" 14 + "tangled.org/oppi.li/atproto-starterkit/appview/config" 15 + "tangled.org/oppi.li/atproto-starterkit/idresolver" 16 + ) 17 + 18 + type OAuth struct { 19 + ClientApp *oauth.ClientApp 20 + SessStore *sessions.CookieStore 21 + Config *config.Config 22 + IdResolver *idresolver.Resolver 23 + Logger *log.Logger 24 + } 25 + 26 + func New(config *config.Config, res *idresolver.Resolver, logger *log.Logger) (*OAuth, error) { 27 + var oauthConfig oauth.ClientConfig 28 + var clientUri string 29 + 30 + if config.Core.Dev { 31 + clientUri = "http://" + net.JoinHostPort("127.0.0.1", config.Core.Port()) 32 + callbackUri := clientUri + "/oauth/callback" 33 + oauthConfig = oauth.NewLocalhostConfig(callbackUri, Scopes) 34 + } else { 35 + clientUri = config.Core.Host 36 + clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri) 37 + callbackUri := clientUri + "/oauth/callback" 38 + oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, Scopes) 39 + } 40 + 41 + // configure client secret 42 + priv, err := atcrypto.ParsePrivateMultibase(config.OAuth.ClientSecret) 43 + if err != nil { 44 + return nil, err 45 + } 46 + if err := oauthConfig.SetClientSecret(priv, config.OAuth.ClientKid); err != nil { 47 + return nil, err 48 + } 49 + 50 + authStore, err := NewRedisStore(&RedisStoreConfig{ 51 + RedisURL: config.Redis.ToURL(), 52 + SessionExpiryDuration: time.Hour * 24 * 90, 53 + SessionInactivityDuration: time.Hour * 24 * 14, 54 + AuthRequestExpiryDuration: time.Minute * 30, 55 + }) 56 + if err != nil { 57 + return nil, err 58 + } 59 + 60 + sessStore := sessions.NewCookieStore([]byte(config.Core.CookieSecret)) 61 + 62 + clientApp := oauth.NewClientApp(&oauthConfig, authStore) 63 + clientApp.Dir = res.Directory() 64 + 65 + // allow non-public transports in dev mode 66 + if config.Core.Dev { 67 + clientApp.Resolver.Client.Transport = http.DefaultTransport 68 + } 69 + 70 + logger.Info("oauth setup successfully", "confidential", clientApp.Config.IsConfidential()) 71 + return &OAuth{ 72 + ClientApp: clientApp, 73 + Config: config, 74 + SessStore: sessStore, 75 + IdResolver: res, 76 + Logger: logger, 77 + }, nil 78 + } 79 + 80 + func (o *OAuth) AuthorizedClient(r *http.Request) (*atpclient.APIClient, error) { 81 + session, err := o.ResumeSession(r) 82 + if err != nil { 83 + return nil, fmt.Errorf("error getting session: %w", err) 84 + } 85 + return session.APIClient(), nil 86 + }
+234
appview/oauth/session.go
··· 1 + package oauth 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.org/oppi.li/atproto-starterkit/appview/types" 11 + ) 12 + 13 + const ( 14 + SessionName = "appview_session" 15 + SessionDid = "did" 16 + SessionPds = "pds" 17 + SessionId = "session_id" 18 + SessionAuthenticated = "authenticated" 19 + AccountsName = "appview_accounts" 20 + AuthReturnName = "appview_auth_return" 21 + AuthReturnURL = "return_url" 22 + AuthAddAccount = "add_account" 23 + ) 24 + 25 + var Scopes = []string{"atproto", "transition:generic"} 26 + 27 + func (o *OAuth) GetAccounts(r *http.Request) *types.AccountRegistry { 28 + session, err := o.SessStore.Get(r, AccountsName) 29 + if err != nil || session.IsNew { 30 + return &types.AccountRegistry{Accounts: []types.AccountInfo{}} 31 + } 32 + 33 + data, ok := session.Values["accounts"].(string) 34 + if !ok { 35 + return &types.AccountRegistry{Accounts: []types.AccountInfo{}} 36 + } 37 + 38 + var registry types.AccountRegistry 39 + if err := json.Unmarshal([]byte(data), &registry); err != nil { 40 + return &types.AccountRegistry{Accounts: []types.AccountInfo{}} 41 + } 42 + 43 + return &registry 44 + } 45 + 46 + func (o *OAuth) SaveAccounts(w http.ResponseWriter, r *http.Request, registry *types.AccountRegistry) error { 47 + session, err := o.SessStore.Get(r, AccountsName) 48 + if err != nil { 49 + return err 50 + } 51 + 52 + data, err := json.Marshal(registry) 53 + if err != nil { 54 + return err 55 + } 56 + 57 + session.Values["accounts"] = string(data) 58 + session.Options.MaxAge = 60 * 60 * 24 * 90 59 + session.Options.HttpOnly = true 60 + session.Options.Secure = !o.Config.Core.Dev 61 + session.Options.SameSite = http.SameSiteLaxMode 62 + 63 + return session.Save(r, w) 64 + } 65 + 66 + func (o *OAuth) GetUser(r *http.Request) *types.User { 67 + sess, err := o.ResumeSession(r) 68 + if err != nil { 69 + return nil 70 + } 71 + 72 + return &types.User{ 73 + Did: sess.Data.AccountDID.String(), 74 + Pds: sess.Data.HostURL, 75 + } 76 + } 77 + 78 + func (o *OAuth) GetMultiAccountUser(r *http.Request) *types.MultiAccountUser { 79 + user := o.GetUser(r) 80 + if user == nil { 81 + return nil 82 + } 83 + 84 + registry := o.GetAccounts(r) 85 + return &types.MultiAccountUser{ 86 + Active: user, 87 + Accounts: registry.Accounts, 88 + } 89 + } 90 + 91 + func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error { 92 + userSession, err := o.SessStore.Get(r, SessionName) 93 + if err != nil { 94 + return err 95 + } 96 + 97 + userSession.Values[SessionDid] = sessData.AccountDID.String() 98 + userSession.Values[SessionPds] = sessData.HostURL 99 + userSession.Values[SessionId] = sessData.SessionID 100 + userSession.Values[SessionAuthenticated] = true 101 + 102 + if err := userSession.Save(r, w); err != nil { 103 + return err 104 + } 105 + 106 + registry := o.GetAccounts(r) 107 + if err := registry.AddAccount(sessData.AccountDID.String(), sessData.SessionID); err != nil { 108 + return err 109 + } 110 + return o.SaveAccounts(w, r, registry) 111 + } 112 + 113 + func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) { 114 + userSession, err := o.SessStore.Get(r, SessionName) 115 + if err != nil { 116 + return nil, fmt.Errorf("error getting user session: %w", err) 117 + } 118 + if userSession.IsNew { 119 + return nil, fmt.Errorf("no session available for user") 120 + } 121 + 122 + d := userSession.Values[SessionDid].(string) 123 + sessDid, err := syntax.ParseDID(d) 124 + if err != nil { 125 + return nil, fmt.Errorf("malformed did in session cookie '%s': %w", d, err) 126 + } 127 + 128 + sessId := userSession.Values[SessionId].(string) 129 + 130 + clientSess, err := o.ClientApp.ResumeSession(r.Context(), sessDid, sessId) 131 + if err != nil { 132 + return nil, fmt.Errorf("failed to resume session: %w", err) 133 + } 134 + 135 + return clientSess, nil 136 + } 137 + 138 + func (o *OAuth) DeleteSession(w http.ResponseWriter, r *http.Request) error { 139 + userSession, err := o.SessStore.Get(r, SessionName) 140 + if err != nil { 141 + return fmt.Errorf("error getting user session: %w", err) 142 + } 143 + if userSession.IsNew { 144 + return fmt.Errorf("no session available for user") 145 + } 146 + 147 + d := userSession.Values[SessionDid].(string) 148 + sessDid, err := syntax.ParseDID(d) 149 + if err != nil { 150 + return fmt.Errorf("malformed did in session cookie '%s': %w", d, err) 151 + } 152 + 153 + sessId := userSession.Values[SessionId].(string) 154 + 155 + if err := o.ClientApp.Logout(r.Context(), sessDid, sessId); err != nil { 156 + o.Logger.Warn("failed to logout", "err", err) 157 + } 158 + 159 + userSession.Options.MaxAge = -1 160 + return o.SessStore.Save(r, w, userSession) 161 + } 162 + 163 + func (o *OAuth) SwitchAccount(w http.ResponseWriter, r *http.Request, targetDid string) error { 164 + registry := o.GetAccounts(r) 165 + account := registry.FindAccount(targetDid) 166 + if account == nil { 167 + return fmt.Errorf("account not found in registry: %s", targetDid) 168 + } 169 + 170 + did, err := syntax.ParseDID(targetDid) 171 + if err != nil { 172 + return fmt.Errorf("invalid did: %w", err) 173 + } 174 + 175 + sess, err := o.ClientApp.ResumeSession(r.Context(), did, account.SessionId) 176 + if err != nil { 177 + registry.RemoveAccount(targetDid) 178 + _ = o.SaveAccounts(w, r, registry) 179 + return fmt.Errorf("session expired for account: %w", err) 180 + } 181 + 182 + userSession, err := o.SessStore.Get(r, SessionName) 183 + if err != nil { 184 + return err 185 + } 186 + 187 + userSession.Values[SessionDid] = sess.Data.AccountDID.String() 188 + userSession.Values[SessionPds] = sess.Data.HostURL 189 + userSession.Values[SessionId] = sess.Data.SessionID 190 + userSession.Values[SessionAuthenticated] = true 191 + 192 + return userSession.Save(r, w) 193 + } 194 + 195 + func (o *OAuth) SetAuthReturn(w http.ResponseWriter, r *http.Request, returnURL string, addAccount bool) error { 196 + session, err := o.SessStore.Get(r, AuthReturnName) 197 + if err != nil { 198 + return err 199 + } 200 + 201 + session.Values[AuthReturnURL] = returnURL 202 + session.Values[AuthAddAccount] = addAccount 203 + session.Options.MaxAge = 60 * 30 204 + session.Options.HttpOnly = true 205 + session.Options.Secure = !o.Config.Core.Dev 206 + session.Options.SameSite = http.SameSiteLaxMode 207 + 208 + return session.Save(r, w) 209 + } 210 + 211 + func (o *OAuth) GetAuthReturn(r *http.Request) *types.AuthReturnInfo { 212 + session, err := o.SessStore.Get(r, AuthReturnName) 213 + if err != nil || session.IsNew { 214 + return &types.AuthReturnInfo{} 215 + } 216 + 217 + returnURL, _ := session.Values[AuthReturnURL].(string) 218 + addAccount, _ := session.Values[AuthAddAccount].(bool) 219 + 220 + return &types.AuthReturnInfo{ 221 + ReturnURL: returnURL, 222 + AddAccount: addAccount, 223 + } 224 + } 225 + 226 + func (o *OAuth) ClearAuthReturn(w http.ResponseWriter, r *http.Request) error { 227 + session, err := o.SessStore.Get(r, AuthReturnName) 228 + if err != nil { 229 + return err 230 + } 231 + 232 + session.Options.MaxAge = -1 233 + return session.Save(r, w) 234 + }
+118
appview/oauth/store.go
··· 1 + package oauth 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/redis/go-redis/v9" 12 + ) 13 + 14 + type RedisStoreConfig struct { 15 + RedisURL string 16 + SessionExpiryDuration time.Duration 17 + SessionInactivityDuration time.Duration 18 + AuthRequestExpiryDuration time.Duration 19 + } 20 + 21 + type RedisStore struct { 22 + client *redis.Client 23 + sessionExpiryDuration time.Duration 24 + sessionInactivityDuration time.Duration 25 + authRequestExpiryDuration time.Duration 26 + } 27 + 28 + func NewRedisStore(cfg *RedisStoreConfig) (*RedisStore, error) { 29 + opts, err := redis.ParseURL(cfg.RedisURL) 30 + if err != nil { 31 + return nil, err 32 + } 33 + 34 + client := redis.NewClient(opts) 35 + if err := client.Ping(context.Background()).Err(); err != nil { 36 + return nil, fmt.Errorf("redis ping failed: %w", err) 37 + } 38 + 39 + return &RedisStore{ 40 + client: client, 41 + sessionExpiryDuration: cfg.SessionExpiryDuration, 42 + sessionInactivityDuration: cfg.SessionInactivityDuration, 43 + authRequestExpiryDuration: cfg.AuthRequestExpiryDuration, 44 + }, nil 45 + } 46 + 47 + func (s *RedisStore) sessionKey(did syntax.DID, sessionID string) string { 48 + return fmt.Sprintf("session:%s:%s", did.String(), sessionID) 49 + } 50 + 51 + func (s *RedisStore) requestKey(state string) string { 52 + return fmt.Sprintf("authreq:%s", state) 53 + } 54 + 55 + func (s *RedisStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { 56 + key := s.sessionKey(did, sessionID) 57 + data, err := s.client.Get(ctx, key).Bytes() 58 + if err == redis.Nil { 59 + return nil, err 60 + } 61 + if err != nil { 62 + return nil, err 63 + } 64 + 65 + var session oauth.ClientSessionData 66 + if err := json.Unmarshal(data, &session); err != nil { 67 + return nil, err 68 + } 69 + 70 + // refresh ttl on access 71 + s.client.Expire(ctx, key, s.sessionInactivityDuration) 72 + return &session, nil 73 + } 74 + 75 + func (s *RedisStore) SaveSession(ctx context.Context, session oauth.ClientSessionData) error { 76 + key := s.sessionKey(session.AccountDID, session.SessionID) 77 + data, err := json.Marshal(session) 78 + if err != nil { 79 + return err 80 + } 81 + return s.client.Set(ctx, key, data, s.sessionExpiryDuration).Err() 82 + } 83 + 84 + func (s *RedisStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { 85 + key := s.sessionKey(did, sessionID) 86 + return s.client.Del(ctx, key).Err() 87 + } 88 + 89 + func (s *RedisStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { 90 + key := s.requestKey(state) 91 + data, err := s.client.Get(ctx, key).Bytes() 92 + if err == redis.Nil { 93 + return nil, err 94 + } 95 + if err != nil { 96 + return nil, err 97 + } 98 + 99 + var req oauth.AuthRequestData 100 + if err := json.Unmarshal(data, &req); err != nil { 101 + return nil, err 102 + } 103 + return &req, nil 104 + } 105 + 106 + func (s *RedisStore) SaveAuthRequestInfo(ctx context.Context, req oauth.AuthRequestData) error { 107 + key := s.requestKey(req.State) 108 + data, err := json.Marshal(req) 109 + if err != nil { 110 + return err 111 + } 112 + return s.client.Set(ctx, key, data, s.authRequestExpiryDuration).Err() 113 + } 114 + 115 + func (s *RedisStore) DeleteAuthRequestInfo(ctx context.Context, state string) error { 116 + key := s.requestKey(state) 117 + return s.client.Del(ctx, key).Err() 118 + }
+86
appview/pages/home.go
··· 1 + package pages 2 + 3 + import ( 4 + "context" 5 + 6 + . "maragu.dev/gomponents" 7 + . "maragu.dev/gomponents/html" 8 + "tangled.org/oppi.li/atproto-starterkit/appview/types" 9 + ) 10 + 11 + func (p *Pages) Home(ctx context.Context, user *types.MultiAccountUser) Node { 12 + var activeDid string 13 + var accounts []types.AccountInfo 14 + 15 + if user != nil && user.Active != nil { 16 + activeDid = user.Active.Did 17 + accounts = user.Accounts 18 + } 19 + 20 + isLoggedIn := activeDid != "" 21 + 22 + var rows []Node 23 + if isLoggedIn { 24 + for _, acc := range accounts { 25 + isActive := acc.Did == activeDid 26 + handle := p.resolve(ctx, acc.Did) 27 + 28 + rows = append(rows, Tr( 29 + Td(Style("border: 1px solid #ddd; padding: 8px;"), 30 + If(isActive, Strong(Text(handle))), 31 + If(!isActive, Text(handle)), 32 + ), 33 + Td(Style("border: 1px solid #ddd; padding: 8px;"), Text(acc.Did)), 34 + Td(Style("border: 1px solid #ddd; padding: 8px;"), 35 + If(isActive, Text("active")), 36 + ), 37 + Td(Style("border: 1px solid #ddd; padding: 8px;"), 38 + If(!isActive, 39 + FormEl(Method("post"), Action("/account/switch"), Style("display: inline; margin-right: 5px;"), 40 + Input(Type("hidden"), Name("did"), Value(acc.Did)), 41 + Button(Type("submit"), Text("switch")), 42 + ), 43 + ), 44 + FormEl(Method("post"), Action("/account/"+acc.Did), Style("display: inline;"), 45 + Input(Type("hidden"), Name("_method"), Value("DELETE")), 46 + Button(Type("submit"), Text("remove")), 47 + ), 48 + ), 49 + )) 50 + } 51 + } 52 + 53 + borderStyle := Style("border: 1px solid #ddd; padding: 8px;") 54 + 55 + return Layout("ATProto Starter Kit", 56 + H1(Text("ATProto Starter Kit")), 57 + 58 + If(!isLoggedIn, 59 + Group([]Node{ 60 + P(Text("a simple atproto appview starter kit.")), 61 + A(Href("/login"), Text("Login")), 62 + }), 63 + ), 64 + 65 + If(isLoggedIn, 66 + Group([]Node{ 67 + H2(Text("Accounts")), 68 + Table( 69 + THead( 70 + Tr( 71 + Th(borderStyle, Text("Handle")), 72 + Th(borderStyle, Text("DID")), 73 + Th(borderStyle, Text("Status")), 74 + Th(borderStyle, Text("Actions")), 75 + ), 76 + ), 77 + TBody(Group(rows)), 78 + ), 79 + 80 + Div( 81 + A(Href("/login"), Text("Add another account")), 82 + ), 83 + }), 84 + ), 85 + ) 86 + }
+21
appview/pages/layout.go
··· 1 + package pages 2 + 3 + import ( 4 + . "maragu.dev/gomponents" 5 + . "maragu.dev/gomponents/html" 6 + ) 7 + 8 + func Layout(title string, content ...Node) Node { 9 + return Doctype( 10 + HTML( 11 + Head( 12 + Meta(Charset("utf-8")), 13 + Meta(Name("viewport"), Content("width=device-width, initial-scale=1")), 14 + TitleEl(Text(title)), 15 + ), 16 + Body( 17 + Group(content), 18 + ), 19 + ), 20 + ) 21 + }
+16
appview/pages/login.go
··· 1 + package pages 2 + 3 + import ( 4 + . "maragu.dev/gomponents" 5 + . "maragu.dev/gomponents/html" 6 + ) 7 + 8 + func (p *Pages) Login() Node { 9 + return Layout("Login", 10 + H1(Text("Login")), 11 + FormEl(Method("post"), 12 + Input(Type("text"), Name("handle"), Placeholder("handle or did"), Required()), 13 + Button(Type("submit"), Text("Login")), 14 + ), 15 + ) 16 + }
+33
appview/pages/pages.go
··· 1 + package pages 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + 7 + . "maragu.dev/gomponents" 8 + "tangled.org/oppi.li/atproto-starterkit/idresolver" 9 + ) 10 + 11 + type Pages struct { 12 + resolver *idresolver.Resolver 13 + } 14 + 15 + func New(resolver *idresolver.Resolver) *Pages { 16 + return &Pages{ 17 + resolver: resolver, 18 + } 19 + } 20 + 21 + func (p *Pages) Render(w http.ResponseWriter, node Node) { 22 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 23 + _ = node.Render(w) 24 + } 25 + 26 + func (p *Pages) resolve(ctx context.Context, did string) string { 27 + ident, err := p.resolver.ResolveIdent(ctx, did) 28 + if err != nil { 29 + return did 30 + } 31 + 32 + return ident.Handle.String() 33 + }
+164
appview/server/handlers.go
··· 1 + package server 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "github.com/go-chi/chi/v5" 9 + ) 10 + 11 + func (s *Server) Home(w http.ResponseWriter, r *http.Request) { 12 + user := s.oauth.GetMultiAccountUser(r) 13 + s.pages.Render(w, s.pages.Home(r.Context(), user)) 14 + } 15 + 16 + func (s *Server) Health(w http.ResponseWriter, r *http.Request) { 17 + w.Header().Set("Content-Type", "application/json") 18 + json.NewEncoder(w).Encode(map[string]string{ 19 + "status": "ok", 20 + }) 21 + } 22 + 23 + func (s *Server) RobotsTxt(w http.ResponseWriter, r *http.Request) { 24 + w.Header().Set("Content-Type", "text/plain") 25 + w.Header().Set("Cache-Control", "public, max-age=86400") 26 + w.Write([]byte(`User-agent: * 27 + Allow: / 28 + Crawl-delay: 1 29 + `)) 30 + } 31 + 32 + // oauth handlers 33 + 34 + func (s *Server) OAuthClientMetadata(w http.ResponseWriter, r *http.Request) { 35 + data := s.oauth.ClientApp.Config.ClientMetadata() 36 + w.Header().Set("Content-Type", "application/json") 37 + json.NewEncoder(w).Encode(data) 38 + } 39 + 40 + func (s *Server) OAuthJWKS(w http.ResponseWriter, r *http.Request) { 41 + data := s.oauth.ClientApp.Config.PublicJWKS() 42 + w.Header().Set("Content-Type", "application/json") 43 + json.NewEncoder(w).Encode(data) 44 + } 45 + 46 + func (s *Server) OAuthCallback(w http.ResponseWriter, r *http.Request) { 47 + sessData, err := s.oauth.ClientApp.ProcessCallback(r.Context(), r.URL.Query()) 48 + if err != nil { 49 + s.logger.Error("oauth callback failed", "err", err) 50 + http.Error(w, "authentication failed", http.StatusInternalServerError) 51 + return 52 + } 53 + 54 + if err := s.oauth.SaveSession(w, r, sessData); err != nil { 55 + s.logger.Error("failed to save session", "err", err) 56 + http.Error(w, "failed to save session", http.StatusInternalServerError) 57 + return 58 + } 59 + 60 + authReturn := s.oauth.GetAuthReturn(r) 61 + _ = s.oauth.ClearAuthReturn(w, r) 62 + 63 + returnURL := "/" 64 + if authReturn.ReturnURL != "" { 65 + returnURL = authReturn.ReturnURL 66 + } 67 + 68 + http.Redirect(w, r, returnURL, http.StatusFound) 69 + } 70 + 71 + func (s *Server) Login(w http.ResponseWriter, r *http.Request) { 72 + if r.Method == http.MethodGet { 73 + addAccount := r.URL.Query().Get("add_account") == "true" 74 + if addAccount { 75 + if err := s.oauth.SetAuthReturn(w, r, "/", true); err != nil { 76 + s.logger.Error("failed to set auth return", "err", err) 77 + } 78 + } 79 + s.pages.Render(w, s.pages.Login()) 80 + return 81 + } 82 + 83 + if err := r.ParseForm(); err != nil { 84 + http.Error(w, "invalid form", http.StatusBadRequest) 85 + return 86 + } 87 + 88 + handle := r.FormValue("handle") 89 + if handle == "" { 90 + http.Error(w, "handle required", http.StatusBadRequest) 91 + return 92 + } 93 + 94 + authUrl, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 95 + if err != nil { 96 + s.logger.Error("failed to start auth", "err", err) 97 + http.Error(w, "authentication failed", http.StatusInternalServerError) 98 + return 99 + } 100 + 101 + http.Redirect(w, r, authUrl, http.StatusFound) 102 + } 103 + 104 + func (s *Server) Logout(w http.ResponseWriter, r *http.Request) { 105 + if err := s.oauth.DeleteSession(w, r); err != nil { 106 + s.logger.Warn("logout error", "err", err) 107 + } 108 + http.Redirect(w, r, "/", http.StatusFound) 109 + } 110 + 111 + func (s *Server) SwitchAccount(w http.ResponseWriter, r *http.Request) { 112 + targetDid := r.FormValue("did") 113 + if targetDid == "" { 114 + http.Error(w, "did required", http.StatusBadRequest) 115 + return 116 + } 117 + 118 + if err := s.oauth.SwitchAccount(w, r, targetDid); err != nil { 119 + s.logger.Error("failed to switch account", "err", err) 120 + http.Error(w, "failed to switch account", http.StatusInternalServerError) 121 + return 122 + } 123 + 124 + http.Redirect(w, r, "/", http.StatusFound) 125 + } 126 + 127 + func (s *Server) RemoveAccount(w http.ResponseWriter, r *http.Request) { 128 + targetDid := chi.URLParam(r, "did") 129 + if targetDid == "" { 130 + http.Error(w, "did required", http.StatusBadRequest) 131 + return 132 + } 133 + 134 + user := s.oauth.GetUser(r) 135 + isActiveAccount := user != nil && user.Did == targetDid 136 + 137 + registry := s.oauth.GetAccounts(r) 138 + account := registry.FindAccount(targetDid) 139 + if account == nil { 140 + http.Redirect(w, r, "/", http.StatusFound) 141 + return 142 + } 143 + 144 + did, err := syntax.ParseDID(targetDid) 145 + if err == nil { 146 + _ = s.oauth.ClientApp.Logout(r.Context(), did, account.SessionId) 147 + } 148 + 149 + registry.RemoveAccount(targetDid) 150 + _ = s.oauth.SaveAccounts(w, r, registry) 151 + 152 + // if removing active account, switch to another or logout 153 + if isActiveAccount { 154 + if len(registry.Accounts) > 0 { 155 + // switch to first remaining account 156 + _ = s.oauth.SwitchAccount(w, r, registry.Accounts[0].Did) 157 + } else { 158 + // no accounts left, logout completely 159 + _ = s.oauth.DeleteSession(w, r) 160 + } 161 + } 162 + 163 + http.Redirect(w, r, "/", http.StatusFound) 164 + }
+61
appview/server/router.go
··· 1 + package server 2 + 3 + import ( 4 + "net/http" 5 + "time" 6 + 7 + "github.com/go-chi/chi/v5" 8 + "github.com/go-chi/chi/v5/middleware" 9 + ) 10 + 11 + func (s *Server) Router() http.Handler { 12 + r := chi.NewRouter() 13 + 14 + // middleware 15 + r.Use(middleware.RequestID) 16 + r.Use(middleware.RealIP) 17 + r.Use(s.loggingMiddleware) 18 + r.Use(middleware.Recoverer) 19 + r.Use(middleware.Timeout(60 * time.Second)) 20 + 21 + // basic routes 22 + r.Get("/", s.Home) 23 + r.Get("/health", s.Health) 24 + r.Get("/robots.txt", s.RobotsTxt) 25 + 26 + // oauth routes 27 + r.Get("/login", s.Login) 28 + r.Post("/login", s.Login) 29 + r.Post("/logout", s.Logout) 30 + r.Post("/account/switch", s.SwitchAccount) 31 + r.Post("/account/{did}", s.RemoveAccount) 32 + r.Delete("/account/{did}", s.RemoveAccount) 33 + 34 + r.Get("/oauth/client-metadata.json", s.OAuthClientMetadata) 35 + r.Get("/oauth/jwks.json", s.OAuthJWKS) 36 + r.Get("/oauth/callback", s.OAuthCallback) 37 + 38 + // xrpc routes 39 + r.Mount("/xrpc", s.XRPCRouter()) 40 + 41 + return r 42 + } 43 + 44 + func (s *Server) loggingMiddleware(next http.Handler) http.Handler { 45 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 + start := time.Now() 47 + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) 48 + 49 + defer func() { 50 + s.logger.Info("request", 51 + "method", r.Method, 52 + "path", r.URL.Path, 53 + "status", ww.Status(), 54 + "bytes", ww.BytesWritten(), 55 + "duration", time.Since(start), 56 + ) 57 + }() 58 + 59 + next.ServeHTTP(ww, r) 60 + }) 61 + }
+60
appview/server/server.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + clog "github.com/charmbracelet/log" 8 + "tangled.org/oppi.li/atproto-starterkit/appview/config" 9 + "tangled.org/oppi.li/atproto-starterkit/appview/db" 10 + "tangled.org/oppi.li/atproto-starterkit/appview/oauth" 11 + "tangled.org/oppi.li/atproto-starterkit/appview/pages" 12 + "tangled.org/oppi.li/atproto-starterkit/idresolver" 13 + "tangled.org/oppi.li/atproto-starterkit/log" 14 + ) 15 + 16 + type Server struct { 17 + db *db.DB 18 + oauth *oauth.OAuth 19 + idResolver *idresolver.Resolver 20 + pages *pages.Pages 21 + config *config.Config 22 + logger *clog.Logger 23 + } 24 + 25 + func Make(ctx context.Context, config *config.Config) (*Server, error) { 26 + logger := log.FromContext(ctx) 27 + 28 + d, err := db.Make(ctx, config.Core.DbPath, log.SubLogger(logger, "db")) 29 + if err != nil { 30 + return nil, fmt.Errorf("failed to create db: %w", err) 31 + } 32 + 33 + res, err := idresolver.RedisResolver(config.Redis.ToURL(), config.Plc.PLCURL) 34 + if err != nil { 35 + logger.Error("failed to create redis resolver", "err", err) 36 + res = idresolver.DefaultResolver(config.Plc.PLCURL) 37 + } 38 + 39 + oauth, err := oauth.New(config, res, log.SubLogger(logger, "oauth")) 40 + if err != nil { 41 + return nil, fmt.Errorf("failed to start oauth handler: %w", err) 42 + } 43 + 44 + pages := pages.New(res) 45 + 46 + server := &Server{ 47 + d, 48 + oauth, 49 + res, 50 + pages, 51 + config, 52 + logger, 53 + } 54 + 55 + return server, nil 56 + } 57 + 58 + func (s *Server) Close() error { 59 + return s.db.Close() 60 + }
+25
appview/server/xrpc.go
··· 1 + package server 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "github.com/go-chi/chi/v5" 8 + ) 9 + 10 + // xrpc router handles all xrpc methods 11 + func (s *Server) XRPCRouter() http.Handler { 12 + r := chi.NewRouter() 13 + 14 + return r 15 + } 16 + 17 + // helper to write xrpc errors 18 + func (s *Server) xrpcError(w http.ResponseWriter, errType, message string, status int) { 19 + w.Header().Set("Content-Type", "application/json") 20 + w.WriteHeader(status) 21 + json.NewEncoder(w).Encode(map[string]any{ 22 + "error": errType, 23 + "message": message, 24 + }) 25 + }
+98
appview/types/types.go
··· 1 + package types 2 + 3 + import ( 4 + "errors" 5 + "time" 6 + ) 7 + 8 + const MaxAccounts = 20 9 + 10 + var ErrMaxAccountsReached = errors.New("maximum number of linked accounts reached") 11 + 12 + type AccountInfo struct { 13 + Did string `json:"did"` 14 + SessionId string `json:"session_id"` 15 + AddedAt int64 `json:"added_at"` 16 + } 17 + 18 + type AccountRegistry struct { 19 + Accounts []AccountInfo `json:"accounts"` 20 + } 21 + 22 + func (r *AccountRegistry) AddAccount(did, sessionId string) error { 23 + for i, acc := range r.Accounts { 24 + if acc.Did == did { 25 + r.Accounts[i].SessionId = sessionId 26 + return nil 27 + } 28 + } 29 + 30 + if len(r.Accounts) >= MaxAccounts { 31 + return ErrMaxAccountsReached 32 + } 33 + 34 + r.Accounts = append(r.Accounts, AccountInfo{ 35 + Did: did, 36 + SessionId: sessionId, 37 + AddedAt: time.Now().Unix(), 38 + }) 39 + return nil 40 + } 41 + 42 + func (r *AccountRegistry) RemoveAccount(did string) { 43 + filtered := make([]AccountInfo, 0, len(r.Accounts)) 44 + for _, acc := range r.Accounts { 45 + if acc.Did != did { 46 + filtered = append(filtered, acc) 47 + } 48 + } 49 + r.Accounts = filtered 50 + } 51 + 52 + func (r *AccountRegistry) FindAccount(did string) *AccountInfo { 53 + for i := range r.Accounts { 54 + if r.Accounts[i].Did == did { 55 + return &r.Accounts[i] 56 + } 57 + } 58 + return nil 59 + } 60 + 61 + func (r *AccountRegistry) OtherAccounts(activeDid string) []AccountInfo { 62 + result := make([]AccountInfo, 0, len(r.Accounts)) 63 + for _, acc := range r.Accounts { 64 + if acc.Did != activeDid { 65 + result = append(result, acc) 66 + } 67 + } 68 + return result 69 + } 70 + 71 + type MultiAccountUser struct { 72 + Active *User 73 + Accounts []AccountInfo 74 + } 75 + 76 + func (m *MultiAccountUser) Did() string { 77 + if m.Active == nil { 78 + return "" 79 + } 80 + return m.Active.Did 81 + } 82 + 83 + func (m *MultiAccountUser) Pds() string { 84 + if m.Active == nil { 85 + return "" 86 + } 87 + return m.Active.Pds 88 + } 89 + 90 + type User struct { 91 + Did string 92 + Pds string 93 + } 94 + 95 + type AuthReturnInfo struct { 96 + ReturnURL string 97 + AddAccount bool 98 + }
+51
cmd/appview/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + "os" 7 + 8 + "tangled.org/oppi.li/atproto-starterkit/appview/config" 9 + "tangled.org/oppi.li/atproto-starterkit/appview/server" 10 + "tangled.org/oppi.li/atproto-starterkit/log" 11 + ) 12 + 13 + func main() { 14 + ctx := context.Background() 15 + logger := log.New("appview") 16 + ctx = log.IntoContext(ctx, logger) 17 + 18 + c, err := config.LoadConfig(ctx) 19 + if err != nil { 20 + logger.Error("failed to load config", "error", err) 21 + return 22 + } 23 + 24 + logger.Info("config loaded", 25 + "dev", c.Core.Dev, 26 + "host", c.Core.Host, 27 + "listen_addr", c.Core.ListenAddr, 28 + "db_path", c.Core.DbPath, 29 + "redis_addr", c.Redis.Addr, 30 + "plc_url", c.Plc.PLCURL, 31 + "oauth_kid", c.OAuth.ClientKid, 32 + ) 33 + 34 + srv, err := server.Make(ctx, c) 35 + if err != nil { 36 + logger.Error("failed to start appview", "err", err) 37 + os.Exit(-1) 38 + } 39 + 40 + defer func() { 41 + if err := srv.Close(); err != nil { 42 + logger.Error("failed to close server", "err", err) 43 + } 44 + }() 45 + 46 + logger.Info("starting server", "address", c.Core.ListenAddr) 47 + 48 + if err := http.ListenAndServe(c.Core.ListenAddr, srv.Router()); err != nil { 49 + logger.Error("failed to start appview", "err", err) 50 + } 51 + }
+57
go.mod
··· 1 + module tangled.org/oppi.li/atproto-starterkit 2 + 3 + go 1.25.5 4 + 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 7 + github.com/carlmjohnson/versioninfo v0.22.5 8 + github.com/charmbracelet/log v0.4.2 9 + github.com/go-chi/chi/v5 v5.2.0 10 + github.com/gorilla/sessions v1.4.0 11 + github.com/mattn/go-sqlite3 v1.14.24 12 + github.com/redis/go-redis/v9 v9.7.3 13 + github.com/sethvargo/go-envconfig v1.1.0 14 + ) 15 + 16 + require ( 17 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 18 + github.com/beorn7/perks v1.0.1 // indirect 19 + github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 21 + github.com/charmbracelet/lipgloss v1.1.0 // indirect 22 + github.com/charmbracelet/x/ansi v0.8.0 // indirect 23 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 24 + github.com/charmbracelet/x/term v0.2.1 // indirect 25 + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 26 + github.com/go-logfmt/logfmt v0.6.0 // indirect 27 + github.com/go-redis/cache/v9 v9.0.0 // indirect 28 + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 29 + github.com/google/go-querystring v1.1.0 // indirect 30 + github.com/gorilla/securecookie v1.1.2 // indirect 31 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 32 + github.com/klauspost/compress v1.17.3 // indirect 33 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 34 + github.com/mattn/go-isatty v0.0.20 // indirect 35 + github.com/mattn/go-runewidth v0.0.16 // indirect 36 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 37 + github.com/mr-tron/base58 v1.2.0 // indirect 38 + github.com/muesli/termenv v0.16.0 // indirect 39 + github.com/prometheus/client_golang v1.17.0 // indirect 40 + github.com/prometheus/client_model v0.5.0 // indirect 41 + github.com/prometheus/common v0.45.0 // indirect 42 + github.com/prometheus/procfs v0.12.0 // indirect 43 + github.com/rivo/uniseg v0.4.7 // indirect 44 + github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 45 + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 46 + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 47 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 48 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 49 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 50 + golang.org/x/crypto v0.21.0 // indirect 51 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect 52 + golang.org/x/sync v0.7.0 // indirect 53 + golang.org/x/sys v0.30.0 // indirect 54 + golang.org/x/time v0.3.0 // indirect 55 + google.golang.org/protobuf v1.33.0 // indirect 56 + maragu.dev/gomponents v1.2.0 // indirect 57 + )
+376
go.sum
··· 1 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU= 6 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= 7 + github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 8 + github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 9 + github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 10 + github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 11 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 12 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 13 + github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 14 + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 15 + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 16 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 17 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 18 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 19 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 20 + github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= 21 + github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 22 + github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 23 + github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 24 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 25 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 26 + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 27 + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 28 + github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 29 + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 30 + github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 31 + github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 32 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 34 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 36 + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 37 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 38 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 39 + github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 40 + github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 41 + github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 42 + github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= 43 + github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 44 + github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 45 + github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 46 + github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 47 + github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 48 + github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 49 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 50 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 51 + github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= 52 + github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= 53 + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 54 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 55 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 56 + github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 57 + github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 58 + github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 59 + github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 60 + github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 61 + github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 62 + github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 63 + github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 64 + github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 65 + github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 66 + github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 67 + github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 68 + github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 69 + github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 + github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 71 + github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 72 + github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 73 + github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 74 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 75 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 76 + github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 77 + github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 78 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 79 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 80 + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 81 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 82 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 83 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 84 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 85 + github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 86 + github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 87 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 88 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 89 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 90 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 91 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 92 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 93 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 94 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 95 + github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 96 + github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 97 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 98 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 99 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 100 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 101 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 102 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 103 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 104 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 105 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 106 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 107 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 108 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 109 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 110 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 111 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 112 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 113 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 114 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 115 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 116 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 117 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 118 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 119 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 120 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 121 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 122 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 123 + github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 124 + github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= 125 + github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 126 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 127 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 128 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 129 + github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 130 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 131 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 132 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 133 + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 134 + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 135 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 136 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 137 + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 138 + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 139 + github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 140 + github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 141 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 142 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 143 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 144 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 145 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 146 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 147 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 148 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 149 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 150 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 151 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 152 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 153 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 154 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 155 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 156 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 157 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 158 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 159 + github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 160 + github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 161 + github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 162 + github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 163 + github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 164 + github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 165 + github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 166 + github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 167 + github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 168 + github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 169 + github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 170 + github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= 171 + github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 172 + github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 173 + github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 174 + github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 175 + github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 176 + github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 177 + github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 178 + github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 179 + github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 180 + github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 181 + github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 182 + github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 183 + github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= 184 + github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 185 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 186 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 187 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 188 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 189 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 190 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 191 + github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 192 + github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 193 + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 194 + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 195 + github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 196 + github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 197 + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 198 + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 199 + github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 200 + github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 201 + github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 202 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 203 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 204 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 205 + github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 206 + github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= 207 + github.com/sethvargo/go-envconfig v1.1.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= 208 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 209 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 210 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 211 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 212 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 213 + github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 214 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 215 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 216 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 217 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 218 + github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 219 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 220 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 221 + github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 222 + github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 223 + github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 224 + github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 225 + github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 226 + github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 227 + github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 228 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 229 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 230 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 231 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 232 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 233 + github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 234 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 235 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 236 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 237 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 238 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 239 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 240 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 241 + go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 242 + go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 243 + go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 244 + go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 245 + go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 246 + go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 247 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 248 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 249 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 250 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 251 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 252 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 253 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 254 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 255 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 256 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 257 + golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 258 + golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 259 + golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 260 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 261 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 262 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 263 + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 264 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 265 + golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 266 + golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 267 + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 268 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 269 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 270 + golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 271 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 272 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 273 + golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 274 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 275 + golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 276 + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 277 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 278 + golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 279 + golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 280 + golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 281 + golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 282 + golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 283 + golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 284 + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 285 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 286 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 287 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 288 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 289 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 290 + golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 291 + golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 292 + golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 293 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 294 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 + golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 + golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 + golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 + golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 + golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 + golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 + golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 304 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 305 + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 306 + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 307 + golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 308 + golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 309 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 310 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 311 + golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 312 + golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 313 + golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 314 + golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 315 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 316 + golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 317 + golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 318 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 319 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 320 + golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 321 + golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 322 + golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 323 + golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 324 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 325 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 326 + golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 327 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 328 + golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 329 + golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 330 + golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 331 + golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 332 + golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 333 + golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 334 + golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 335 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 336 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 337 + golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 338 + golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 339 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 340 + golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 341 + golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 342 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 343 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 344 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 345 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 346 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 347 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 348 + google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 349 + google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 350 + google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 351 + google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 352 + google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 353 + google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 354 + google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 355 + google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 356 + google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 357 + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 358 + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 359 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 360 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 361 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 362 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 363 + gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 364 + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 365 + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 366 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 367 + gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 368 + gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 369 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 370 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 371 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 372 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 373 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 374 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 375 + maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc= 376 + maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
+120
idresolver/resolver.go
··· 1 + package idresolver 2 + 3 + import ( 4 + "context" 5 + "net" 6 + "net/http" 7 + "sync" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/atproto/identity" 11 + "github.com/bluesky-social/indigo/atproto/identity/redisdir" 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "github.com/carlmjohnson/versioninfo" 14 + ) 15 + 16 + type Resolver struct { 17 + directory identity.Directory 18 + } 19 + 20 + func BaseDirectory(plcUrl string) identity.Directory { 21 + base := identity.BaseDirectory{ 22 + PLCURL: plcUrl, 23 + HTTPClient: http.Client{ 24 + Timeout: time.Second * 10, 25 + Transport: &http.Transport{ 26 + IdleConnTimeout: time.Millisecond * 1000, 27 + MaxIdleConns: 100, 28 + }, 29 + }, 30 + Resolver: net.Resolver{ 31 + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 32 + d := net.Dialer{Timeout: time.Second * 3} 33 + return d.DialContext(ctx, network, address) 34 + }, 35 + }, 36 + TryAuthoritativeDNS: true, 37 + SkipDNSDomainSuffixes: []string{".bsky.social"}, 38 + UserAgent: "atproto-starterkit/" + versioninfo.Short(), 39 + } 40 + return &base 41 + } 42 + 43 + func RedisDirectory(url, plcUrl string) (identity.Directory, error) { 44 + hitTTL := time.Hour * 24 45 + errTTL := time.Second * 30 46 + invalidHandleTTL := time.Minute * 5 47 + return redisdir.NewRedisDirectory( 48 + BaseDirectory(plcUrl), 49 + url, 50 + hitTTL, 51 + errTTL, 52 + invalidHandleTTL, 53 + 10000, 54 + ) 55 + } 56 + 57 + func DefaultResolver(plcUrl string) *Resolver { 58 + base := BaseDirectory(plcUrl) 59 + cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) 60 + return &Resolver{ 61 + directory: &cached, 62 + } 63 + } 64 + 65 + func RedisResolver(redisUrl, plcUrl string) (*Resolver, error) { 66 + directory, err := RedisDirectory(redisUrl, plcUrl) 67 + if err != nil { 68 + return nil, err 69 + } 70 + return &Resolver{ 71 + directory: directory, 72 + }, nil 73 + } 74 + 75 + func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) { 76 + id, err := syntax.ParseAtIdentifier(arg) 77 + if err != nil { 78 + return nil, err 79 + } 80 + return r.directory.Lookup(ctx, *id) 81 + } 82 + 83 + func (r *Resolver) ResolveIdents(ctx context.Context, idents []string) []*identity.Identity { 84 + results := make([]*identity.Identity, len(idents)) 85 + var wg sync.WaitGroup 86 + 87 + done := make(chan struct{}) 88 + defer close(done) 89 + 90 + for idx, ident := range idents { 91 + wg.Add(1) 92 + go func(index int, id string) { 93 + defer wg.Done() 94 + select { 95 + case <-ctx.Done(): 96 + results[index] = nil 97 + case <-done: 98 + results[index] = nil 99 + default: 100 + identity, _ := r.ResolveIdent(ctx, id) 101 + results[index] = identity 102 + } 103 + }(idx, ident) 104 + } 105 + 106 + wg.Wait() 107 + return results 108 + } 109 + 110 + func (r *Resolver) InvalidateIdent(ctx context.Context, arg string) error { 111 + id, err := syntax.ParseAtIdentifier(arg) 112 + if err != nil { 113 + return err 114 + } 115 + return r.directory.Purge(ctx, *id) 116 + } 117 + 118 + func (r *Resolver) Directory() identity.Directory { 119 + return r.directory 120 + }
+38
log/log.go
··· 1 + package log 2 + 3 + import ( 4 + "context" 5 + "os" 6 + 7 + "github.com/charmbracelet/log" 8 + ) 9 + 10 + type contextKey string 11 + 12 + const loggerKey contextKey = "logger" 13 + 14 + func New(component string) *log.Logger { 15 + logger := log.NewWithOptions(os.Stderr, log.Options{ 16 + ReportTimestamp: true, 17 + ReportCaller: false, 18 + }) 19 + if component != "" { 20 + logger = logger.With("component", component) 21 + } 22 + return logger 23 + } 24 + 25 + func SubLogger(parent *log.Logger, component string) *log.Logger { 26 + return parent.With("component", component) 27 + } 28 + 29 + func IntoContext(ctx context.Context, logger *log.Logger) context.Context { 30 + return context.WithValue(ctx, loggerKey, logger) 31 + } 32 + 33 + func FromContext(ctx context.Context) *log.Logger { 34 + if logger, ok := ctx.Value(loggerKey).(*log.Logger); ok { 35 + return logger 36 + } 37 + return New("") 38 + }